Previous | Tutorial index | Next

Tutorial 4: Selective Imports – Importing Specific Parts of a Module

Learning Objectives

Import and use specific parts from a module or specific modules from a package.

1. Introduction: The Art of Choosing What You Need

In Tutorial 3, you learned how to bring entire modules into your program using import module_name. This is like bringing an entire toolbox into your workspace. It's safe and organized, but sometimes it's cumbersome. If you only need a single screwdriver, why bring the entire 50-piece tool chest?

Selective imports address this by allowing you to reach into a module (or package) and pull out exactly what you need – whether it's a single function, a specific class, a constant, or even an entire submodule. This results in cleaner, more concise code that clearly communicates exactly which external components you depend on.

However, with great power comes great responsibility. Selective imports, especially the infamous wildcard (*), can lead to messy, bug-prone code if used carelessly. This tutorial will teach you how to wield selective imports effectively and safely.

2. The from ... import ... Syntax

The most common selective import statement allows you to bring specific names directly into your current namespace.

Syntax:

from module_name import name1, name2, name3

What happens when you run this?

  1. Python locates and loads the entire module (the same as import module_name).
  2. Instead of creating a namespace variable named module_name, it extracts the specified attributes (name1, name2, etc.) from the module.
  3. It creates direct references to these names in your current namespace, bound to the extracted objects.

Example:

# Traditional full import import math print(math.sqrt(16)) # 4.0 - Must use dot notation # Selective import from math import sqrt, pi print(sqrt(16)) # 4.0 - Direct use, no prefix! print(pi) # 3.14159 - Direct use!

Key Difference: Direct Access vs. Namespace

Feature import module from module import name
Namespace Creates a separate namespace (module). Injects names directly into your current namespace.
Access Requires dot notation: module.func(). Direct access: func().
Code length Longer (more typing). Shorter, less repetitive.
Clarity Always clear where the function comes from. May be unclear to readers if the source isn't obvious.
Safety Safe from name collisions (uses namespace). Prone to name collisions (names are in your space).

3. Using Aliases with Selective Imports

Just as with full module imports, you can use the as keyword to give a specific imported item an alias. This is particularly useful when:

Syntax:

from module_name import original_name as alias_name

Example:

# Aliasing a long function name from datetime import datetime as dt current_time = dt.now() print(current_time) # Resolving a conflict between two modules from math import factorial as math_fact from my_utils import factorial as util_fact print(math_fact(5)) # 120 print(util_fact(5)) # Output from your custom function

Importing Multiple Items with Aliases

You can mix regular imports and aliases in one line:

from math import pi, sqrt as square_root, sin print(pi) # 3.14159 print(square_root(16)) # 4.0 print(sin(0)) # 0.0

4. Selective Imports from Packages and Submodules

The from ... import ... syntax is not limited to single modules. You can use it to import specific modules from within a package, or even specific functions from deep within a submodule.

4.1. Importing a Module from a Package

Syntax:

from package_name import module_name

Example (using the xml standard library package):

# Instead of: import xml.etree.ElementTree tree = xml.etree.ElementTree.parse("data.xml") # Verbose! # Use selective import: from xml.etree import ElementTree as ET tree = ET.parse("data.xml") # Clean and concise!

4.2. Importing a Submodule from a Nested Subpackage

Syntax:

from package.subpackage import module_name

Example:

# Instead of: import os.path print(os.path.join("folder", "file.txt")) # Use selective import: from os import path print(path.join("folder", "file.txt"))

4.3. Importing a Function from a Deeply Nested Module

Syntax:

from package.subpackage.module import function_name

Example (with a hypothetical company package):

from company.hr.employee import calculate_salary salary = calculate_salary(employee_id=101)

This is the most concise form, but it's also the most "fragile" if your directory structure changes.

5. The Wildcard Import: from module import * (The Evil Twin)

The wildcard import imports all public names from a module (or those listed in __all__ for a package) directly into your current namespace.

Syntax:

from math import * print(sqrt(16)) # Works print(pi) # Works print(sin(0)) # Works

Why is it Strongly Discouraged?

  1. Namespace Pollution: It dumps dozens (or hundreds) of names into your namespace. You might accidentally overwrite your own variables.

    from math import * sqrt = 10 # Oops! You just overwrote the sqrt function. print(sqrt(16)) # TypeError: 'int' object is not callable!
  2. Ambiguity and Readability: When you see sqrt(16) in your code, you (and your collaborators) might not remember if sqrt came from math, numpy, or if it's a local function. This makes the code much harder to understand and debug.

  3. Hidden Dependencies: If you use from math import *, and later you remove the import line (or Python changes the module), your code breaks in mysterious ways. Explicit imports clearly document your dependencies.

  4. Masking Bugs: If a function doesn't exist in the module, you won't know until you try to call it at runtime. With explicit imports, you get an ImportError immediately when the script starts, making debugging easier.

  5. Conflicts with Standard Library: If you import two different modules with *, and they both have a read() function, the later import overwrites the earlier one. This is a nightmare to debug.

The Exception: Interactive Exploration

The only somewhat acceptable use of * is during interactive sessions (like in the REPL, Jupyter notebooks, or IPython) where you are just exploring or prototyping. It saves typing. Even then, it's sloppy. For production code, never use from module import * in your scripts.

Remember: If you need so many things from a module that you're tempted to use *, it's a strong signal that you should import the whole module with import module and use dot notation instead.

6. Comparing import os vs. from os import path

This is a common point of confusion for beginners. Let's break down the practical differences.

Scenario A: import os

import os current_dir = os.getcwd() # Accessing a function from os file_path = os.path.join("a", "b") # Accessing the 'path' submodule via os

Scenario B: from os import path

from os import path file_path = path.join("a", "b") # Accessing 'path' directly # But to use os.getcwd(), you would still need to import os separately or use a different method.
from os import * print(getcwd()) # Works, but where did getcwd come from? print(path.join("a","b")) # Works, but path is a submodule

Best Practice

If you use many functions from a module (more than 3 or 4), use import os. If you only need one specific submodule (like path), and you use it very frequently, from os import path is acceptable, but consider importing the parent module too for clarity.

# Recommended hybrid approach for clarity import os.path # Although this doesn't import os, it imports os.path as a side effect, but you still can't use os.getcwd() # Better: import os from os import path as p # Now you have both os and path current_dir = os.getcwd() full_path = p.join(current_dir, "data.txt")

7. Quiz: Check Your Understanding

Test your foundational knowledge before proceeding to the practical exercises.


Q1: Which import statement allows you to use sqrt(16) directly without a prefix?

A) import math B) import math.sqrt C) from math import sqrt D) math import sqrt

AnswerC) `from math import sqrt`

Q2: What is the main problem with using from module import * in a production script?

A) It is slower than other import methods. B) It pollutes the namespace and makes code unclear. C) It cannot import classes, only functions. D) It requires the as keyword to work.

AnswerB) It pollutes the namespace and makes code unclear.

Q3: You have a package named utils with a submodule math_ops. Which statement imports the math_ops module correctly?

A) import utils.math_ops B) from utils import math_ops C) import math_ops from utils D) from math_ops import utils

AnswerB) `from utils import math_ops`. This imports the submodule `math_ops` directly into your namespace.

Q4: Given the import from datetime import datetime as dt, what is the correct way to call the now() function?

A) datetime.now() B) dt.now() C) datetime.datetime.now() D) datetime.now() as dt

AnswerB) `dt.now()`. You aliased `datetime` (the class) as `dt`.

Q5: True or False: When you use from math import sqrt, Python still loads the entire math module into memory.

AnswerTrue. Python always loads the entire module file (compiles it and executes it) regardless of whether you import everything or just one attribute. The difference is only in what names are placed in your current namespace.

8. Hands-on Exercises (Practical Tasks)

These exercises will help you internalize the differences between various import styles.


Exercise 1: The sqrt and pi Import

Goal: Practice selective imports and observe the direct access behavior.

Instructions:

  1. Open your Python interpreter.
  2. Type from math import sqrt, pi.
  3. Type sqrt(25) and confirm it returns 5.0.
  4. Type pi and confirm it returns 3.14159....
  5. Type math.sqrt(25). What error do you get? Why?
  6. Now, try to use cos(0) from the math module. What error do you get? Why?
Sample Output ``` >>> from math import sqrt, pi >>> sqrt(25) 5.0 >>> pi 3.141592653589793 >>> math.sqrt(25) Traceback (most recent call last): File "", line 1, in NameError: name 'math' is not defined >>> cos(0) Traceback (most recent call last): File "", line 1, in NameError: name 'cos' is not defined ``` The error for `math.sqrt(25)` occurs because `math` is not defined in your namespace; you only imported `sqrt` and `pi`. The error for `cos(0)` occurs because you didn't import `cos`.

Exercise 2: Importing path from os

Goal: Understand how to import a submodule and compare readability.

Instructions:

  1. Import path from os: from os import path.
  2. Use path.join("home", "user", "docs") to create a file path string.
  3. Try to use os.getcwd(). You will get a NameError because os itself is not imported.
  4. Now, in the same interpreter session, type import os. You now have both os and path available.
  5. Compare the two ways to join paths:
  6. Write a short comment in your mind (or on paper) about which style you find more readable and why.
Sample Output ``` >>> from os import path >>> path.join("home", "user", "docs") 'home/user/docs' >>> os.getcwd() Traceback (most recent call last): File "", line 1, in NameError: name 'os' is not defined >>> import os >>> os.path.join("a", "b") 'a/b' ``` Many developers prefer `os.path.join` because it's more explicit about the source of the `path` module. However, if you use `path` very frequently, the shorter `path.join` can improve readability.

Exercise 3: Resolving a Name Conflict with Aliases

Goal: Learn how to use aliases to differentiate between functions from different modules that have the same name.

Instructions:

  1. Import the sqrt function from math and give it the alias math_sqrt.
  2. Create your own local function called sqrt that takes a number x and returns x**3 (cubed).
  3. Now, you have two functions: math_sqrt(x) (from math) and sqrt(x) (your custom cube function).
  4. Call sqrt(3) – it should return 27.
  5. Call math_sqrt(3) – it should return 1.732....
  6. Explain how this alias solved the naming conflict.
Sample Output ``` >>> from math import sqrt as math_sqrt >>> def sqrt(x): ... return x ** 3 ... >>> sqrt(3) 27 >>> math_sqrt(3) 1.7320508075688772 ``` The alias `math_sqrt` allowed us to keep the original `sqrt` function from `math` while also defining our own `sqrt` function. Without the alias, the local function would have overwritten the imported `sqrt`.

Exercise 4: The Danger of * – A Live Demonstration

Goal: Visually see why wildcard imports are dangerous.

Instructions:

  1. In the Python interpreter, define a variable: sum = 100.
  2. Now, type from builtins import * (this imports a lot of built-in functions, including sum).
  3. What happened to your sum variable? Type sum([1, 2, 3]). It returns 6, which is the built-in sum function. Your sum = 100 variable has been completely overwritten!
  4. To fix this, restart the interpreter or explicitly re-define sum = 100 after the import (which is annoying).
  5. This demonstrates how * silently overwrites your existing names without warning.
Sample Output ``` >>> sum = 100 >>> from builtins import * >>> sum([1, 2, 3]) 6 ``` Your `sum` variable was silently overwritten by the built-in `sum` function. This is a classic example of why wildcard imports are dangerous.

9. Common Pitfalls and Troubleshooting

Problem Likely Cause Solution
ImportError: cannot import name 'function_name' The function doesn't exist in the module, or you misspelled it. Check the spelling and case. Use dir(module_name) to see all available names.
You get NameError for a name you thought you imported. You imported it with an alias (as something), but you forgot to use the alias. Double-check your import statement. If you did from math import sqrt as square_root, you must use square_root.
You imported two functions with the same name, and the second overwrote the first. You used from module1 import func and then from module2 import func. Use aliases: from module1 import func as func1 and from module2 import func as func2.
AttributeError: 'module' object has no attribute 'func' You used import module and then tried to call func() without the module. prefix. Remember that import module requires dot notation. Use module.func(). Or switch to from module import func.
You accidentally imported a local variable instead of a module function. You have a variable in your script with the same name as the function you're trying to import. Rename your variable. Python will not warn you if you shadow an imported name; it will just overwrite it.
Trying to import from a package but getting ImportError. The package exists, but the submodule might not be directly importable. Ensure the package has an __init__.py file (for regular packages). Check the correct dotted path.

10. Homework Questions (Take-Home Assignment)

These questions are designed to test your practical ability and deep conceptual understanding.


Homework Question 1: Refactoring a Messy Script

Task: Below is a Python script written by a junior developer. It works, but it's a nightmare of wildcard imports and namespace pollution.

# messy_script.py from math import * from random import * from os import * # Some local configuration random = 42 # A variable named 'random' path = "/home/user" # A variable named 'path' # Calculate something num = sqrt(16) print(f"The square root is {num}") # Random number generation print(randint(1, 10)) # File path joining print(path.join(path, "documents")) # This will fail!

Assignment:

  1. Identify all the problems with this script (there are at least three serious issues).
  2. Rewrite the script correctly. Use proper explicit imports (no wildcard *), use appropriate aliases where necessary, and rename the conflicting variables to make the script work without errors.
  3. Explain in 2-3 sentences why your refactored version is better.
Sample Solution

Problems identified:

  1. Wildcard imports pollute the namespace and make it unclear where functions come from.
  2. The variable random = 42 shadows the random module's functions.
  3. The variable path = "/home/user" shadows the os.path module, causing path.join to fail.
  4. The randint function's origin is unclear (it could be from random or another module).

Refactored script:

import math import random import os # Use clear variable names that don't shadow modules custom_random_value = 42 home_path = "/home/user" # Calculate something num = math.sqrt(16) print(f"The square root is {num}") # Random number generation print(random.randint(1, 10)) # File path joining - using os.path explicitly print(os.path.join(home_path, "documents"))

Why it's better: The refactored version uses explicit imports, so the source of every function is immediately clear. The variable names no longer shadow module names, preventing confusing bugs. This code is more maintainable, readable, and less error-prone for future developers.


Homework Question 2: Building a Geometry Module

Task: Create a Python script that uses selective imports from the math module to perform geometric calculations. The script should:

Requirements:

Submission: Provide the full Python script.

Sample Solution
# geometry.py from math import pi, sin, cos, tan, radians, sqrt def area_of_circle(radius): """Calculate the area of a circle given its radius.""" return pi * radius ** 2 def sine_of_degrees(degrees): """Calculate the sine of an angle given in degrees.""" return sin(radians(degrees)) def hypotenuse(a, b): """Calculate the hypotenuse of a right triangle.""" return sqrt(a**2 + b**2) if __name__ == "__main__": # Test area of circle r = 5 print(f"Area of circle with radius {r}: {area_of_circle(r):.2f}") # Test sine of degrees angle = 30 print(f"Sine of {angle} degrees: {sine_of_degrees(angle):.4f}") # Test hypotenuse a, b = 3, 4 print(f"Hypotenuse of triangle with sides {a} and {b}: {hypotenuse(a, b):.2f}")

Sample output:

Area of circle with radius 5: 78.54 Sine of 30 degrees: 0.5000 Hypotenuse of triangle with sides 3 and 4: 5.00

Homework Question 3: Debugging Selective Import Errors

Task: A developer writes the following code, but it fails with ImportError or NameError. Identify exactly why each line fails and propose a fix.

# buggy_imports.py from datetime import date, timedelta, time import sys # Line 1: This fails today = date.today() # Line 2: This works fine current_time = time.now() # Line 3: This fails one_week = timedelta(days=7) # Line 4: This fails print(sys.platform)

Assignment:

Sample Solution

Line 1 failure: today = date.today()

Line 2 failure: current_time = time.now()

Line 3 failure: one_week = timedelta(days=7)

Line 4 failure: print(sys.platform)

Corrected script:

from datetime import date, timedelta, datetime import sys # Line 1: Works correctly today = date.today() # Line 2: Fixed - use datetime.now() current_time = datetime.now() # Line 3: Works correctly now one_week = timedelta(days=7) # Line 4: Fixed typo print(sys.platform) # Note: 'platform' not 'platform'

Homework Question 4: Designing a Selective Import Strategy

Task: You are working on a large data analysis project. You frequently use the following modules and their components:

Questions:

  1. Would you use import pandas as pd or from pandas import DataFrame, read_csv, concat? Justify your choice based on code length, clarity, and best practices.
  2. For numpy, you use the functions mean and std hundreds of times in your script. Suggest the optimal import strategy.
  3. For matplotlib.pyplot, you only use it in one specific function, but that function calls plot, title, and show multiple times. Suggest an import strategy that keeps the code clean without polluting the global namespace.

Submission: Provide a written explanation (200-300 words) and code snippets demonstrating your import strategy.

Sample Solution

1. Pandas Strategy: I would use import pandas as pd. Although this means typing pd. before every function call, it provides several benefits. First, it's the widely accepted convention among Python data scientists, making code immediately recognizable. Second, it prevents namespace pollution and makes it clear that DataFrame comes from pandas. Third, the alias pd is short enough that the overhead is minimal. Using selective imports like from pandas import DataFrame, read_csv would be acceptable, but if I need more than a handful of pandas functions, import pandas as pd is cleaner.

2. NumPy Strategy: For numpy, since I use mean and std hundreds of times, I would use selective imports with aliases to reduce typing:

from numpy import array, mean as np_mean, std as np_std

This keeps the array function directly accessible while using short aliases for the statistical functions. The aliases np_mean and np_std prevent confusion with any other mean or std functions in the script.

3. Matplotlib Strategy: For matplotlib.pyplot, since it's used only in one function, I would import it inside that function:

def create_plot(data): import matplotlib.pyplot as plt plt.plot(data) plt.title("My Plot") plt.show()

This keeps the import local to the function, avoiding global namespace pollution. It also makes the dependency clear—anyone reading the function can see that it requires matplotlib.pyplot.

Full example:

import pandas as pd from numpy import array, mean as np_mean, std as np_std def create_plot(data): import matplotlib.pyplot as plt plt.plot(data) plt.title("Data Visualization") plt.show() # Usage df = pd.read_csv("data.csv") values = array([1, 2, 3, 4, 5]) mean_val = np_mean(values) print(f"Mean: {mean_val}") create_plot(values)

Homework Question 5: The __all__ and Wildcard Interactions

Task: Consider this package structure:

my_pkg/ ├── __init__.py # Contains: __all__ = ['module_a'] ├── module_a.py # Contains: def a_func(): print("A") ├── module_b.py # Contains: def b_func(): print("B") └── module_c.py # Contains: def c_func(): print("C")

Now, a user writes this script:

from my_pkg import * module_a.a_func() # Works module_b.b_func() # Fails with NameError module_c.c_func() # Fails with NameError

Assignment:

  1. Explain why module_b and module_c are not available even though they exist in the directory.
  2. If the user wanted to use all three modules, what are two different ways they could modify their import statement (without modifying the package's __init__.py)?
  3. If you were the package maintainer and wanted to allow users to use from my_pkg import * to get access to module_a, module_b, and module_c, how would you modify __init__.py?
  4. Provide a short argument (2-3 sentences) for why controlling wildcard imports with __all__ is a responsible design choice.
Sample Solution

1. Why module_b and module_c are not available: The __init__.py file defines __all__ = ['module_a']. When a user runs from my_pkg import *, Python only imports the names listed in __all__. Since module_b and module_c are not in this list, they are not imported into the user's namespace, even though they exist in the directory.

2. Two ways to import all three modules without modifying __init__.py:

3. How to modify __init__.py to allow all three:

# my_pkg/__init__.py __all__ = ['module_a', 'module_b', 'module_c']

4. Why controlling wildcard imports with __all__ is responsible: Defining __all__ gives package authors control over their public API. It prevents users from accidentally importing internal or experimental modules that are not intended for public use. This protects users from relying on implementation details that might change in future versions, ensuring better backward compatibility and cleaner code.

11. Summary of Tutorial 4

This concludes the Tutorial 4. Once you have completed the exercises and homework, you will have a nuanced understanding of when and how to use selective imports effectively and safely. You are now ready to move on to Tutorial 5, where we will dive into the Python Standard Library and explore some of its most widely used modules in detail!

Previous | Tutorial index | Next