Python’s import system solves two related problems: organizing code and deciding where names live. A module provides a namespace for code loaded as a unit. A package organizes related modules and subpackages. An import statement locates and loads a module according to Python’s import machinery, then binds one or more names in the importing namespace. Confusion usually comes from mixing those steps together.
As of September 28, 2026, the Python Institute continues to list PCAP-31-03 as active, while PCAP-31-04 is still shown as in development. Across Python Institute certifications, the import and namespace concepts below are core Python semantics rather than temporary exam-version details.
A module is an object with its own namespace
When Python imports a module, it creates or retrieves a module object. The module has a namespace that contains names created by executing the module’s top-level code. A function defined in that module uses the module’s global namespace when resolving its global names, even if another module imports and calls the function.
This explains why “global” does not mean global across the whole Python process. A global variable is normally global to one module namespace. Two modules can each define a name such as config without sharing the same variable.
Module attributes are accessed with dot notation, such as math.sqrt. That qualification is valuable because it shows where a name came from and avoids collisions with names defined locally.
Different import forms bind different names
import module binds the module object to a name in the current namespace, usually the module’s name. from module import thing imports the module as needed and then binds the selected object directly to the local name thing. The object may be the same, but the local binding is different.
An alias changes the local name: import module as m binds the module to m. Aliases can improve readability for long or conventional module names, but excessive abbreviation can make code harder to understand.
Wildcard imports make name origins difficult to see and can overwrite existing names. They are therefore generally poor practice in application code even though candidates should understand their basic behavior.
Packages organize modules under a shared import path
A package provides a namespace that can contain modules and subpackages. Traditional packages use directories with __init__.py, while modern Python also supports namespace packages that can span multiple locations. For PCAP-level reasoning, the important idea is that dotted names describe a hierarchy of importable components.
Package design helps separate responsibilities. A larger application might put database access, web handlers, validation, and domain logic in different modules rather than one enormous file. The module boundary also creates a natural namespace boundary, reducing accidental name collisions.
Beginner Python programming is useful background. Moving beyond beginner scripts means learning to organize code so imported modules have clear responsibilities and minimal surprising side effects.
Python searches for modules through import finders and paths
For ordinary filesystem imports, the search path represented through sys.path is central. It can include the script location, environment-controlled paths, standard-library locations, and installed site-packages directories. The exact construction depends on how Python starts and on the environment.
A ModuleNotFoundError therefore does not necessarily mean the package is absent from the machine. It may be installed for a different Python interpreter or virtual environment, the current environment may not include its location, or the code may be running from a directory that changes how package-relative paths resolve.
Debug import failures by identifying the interpreter first. Inspect sys.executable and the effective search path before repeatedly reinstalling packages. Many “Python cannot find my package” problems are really environment-selection problems.
sys.modules caches modules that have already been loaded
Python keeps loaded modules in sys.modules. Repeating a normal import usually returns the existing module object instead of executing the module’s top-level code again. That is why a module-level print statement normally appears only on the first import in a process.
This caching behavior also explains why import-time side effects deserve caution. Opening network connections, changing global configuration, or doing expensive work at import time can make program startup unpredictable and complicate tests. Modules should generally define functions, classes, constants, and lightweight initialization rather than perform large application workflows just because they were imported.
Reloading a module is a specialized operation and does not magically update every object previously imported from it. Existing references can still point to older functions or classes. Application design should not rely on reload as a routine deployment mechanism.
Namespaces determine which object a name resolves to
Python name lookup is often introduced through the LEGB idea: local, enclosing, global, and built-in scopes. That model is useful for function-level reasoning. A local assignment normally creates or updates a local name. global tells a function that a name refers to the module namespace. nonlocal targets an enclosing function scope.
Name shadowing is legal but can reduce clarity. A local variable named list can shadow the built-in list constructor. A module variable can shadow an imported name. Code may still run until a later statement expects the original object and finds the shadowing value instead.
Namespaces are one reason qualified access is helpful. module.name makes the source explicit and reduces the number of unqualified names competing in the local scope.
Relative and absolute imports communicate package relationships
Absolute imports name a package path from the import root. Relative imports use leading dots inside a package to refer to nearby modules. Relative imports can express internal package relationships concisely, while absolute imports can make the full dependency clearer to readers.
A common beginner problem appears when a file designed as part of a package is executed directly as a standalone script. Its package context can differ, causing relative imports to fail. Running code through the package’s intended entry point or using -m where appropriate can preserve the package context.
Good package design avoids circular imports where modules need one another’s partially initialized state. Circularity can sometimes be resolved by moving shared definitions to a third module, importing inside a function only where justified, or redesigning responsibilities so dependency direction is clearer.
Use __name__ to distinguish import from direct execution
When a module is imported, its __name__ identifies the module. When a file is executed as the main program, __name__ is set to "__main__". The familiar guard if __name__ == "__main__": therefore lets a file expose reusable definitions without automatically running its command-line workflow when imported.
This pattern is useful for keeping import side effects small. Functions and classes can be defined at module scope, while demonstration code, argument parsing, or application startup lives behind the main guard.
For PCAP preparation, create a small package with two or three modules. Import the same module in different ways, print module names, inspect sys.path and sys.modules, and deliberately create then fix an import collision. The goal is to understand how names are bound and how code is located, not merely to memorize four import syntaxes.
Virtual environments separate installed distributions, not Python’s import semantics
A virtual environment gives a project an isolated interpreter context and package-installation location. It helps prevent one project’s dependency versions from silently changing another project’s runtime. But it does not replace the import system: modules are still found through the active interpreter’s search machinery, and names are still bound according to normal Python rules.
This distinction helps diagnose a frequent failure. A developer activates one virtual environment in a shell but configures an editor, scheduled job, or service to use a different Python executable. Installing the package again in the shell’s environment does not fix the service because the service searches a different site-packages directory. Always identify the interpreter used by the failing process before changing dependencies.
Python IDEs are relevant here because IDE interpreter selection controls which environment an editor uses for execution, completion, and debugging. A correct package can appear “missing” when the tool points at the wrong interpreter.
Import design affects testability and application startup.
Modules that perform large actions during import are difficult to test because simply importing them can open files, connect to services, or modify global state. A better structure defines reusable functions and classes at module scope and lets an explicit entry point perform the application workflow.
This also makes dependency direction easier to see. If module A imports B only for one optional action, consider whether that action belongs behind a function boundary. If two modules import each other because they share data types or constants, moving the shared concept into a smaller third module can break the cycle cleanly.
For debugging, treat an import error as a graph problem: which module is being loaded, which dependency does it request, what names are already initialized, and which path or environment determines where Python searches next? That model is more reliable than repeatedly moving files until the error disappears.
One more useful habit is to keep package names and module names conceptually separate. A distribution installed by a package manager can expose one module, many modules, or a module name that differs from the distribution name. When a command such as pip reports that a distribution is installed but import still fails, inspect the distribution metadata and interpreter environment instead of assuming the import name must match the installation name exactly.