Programming in Python

Chapter 9
Modules, Packages and Environment Management

Bachelors Program in AI & ML
Programming in Python   •   Bachelor

Modules, Packages and Environment Management

Introduction

As Python programs grow, keeping all code in one file becomes difficult to test, reuse, maintain, and distribute. Python addresses this problem through modules and packages. A module provides a reusable unit of code, while a package organizes related modules into a structured namespace.

Reusable code alone is insufficient for professional software development. A project must also specify its dependencies, isolate them from other projects, manage configuration securely, support automated testing, and produce installable distributions. These concerns are particularly important in AI and machine learning, where libraries may have strict version constraints and models must often be reproduced across development, testing, and production systems.

This chapter develops a complete workflow: designing modules, understanding imports and namespaces, creating packages, managing isolated environments, declaring and locking dependencies, testing code, auditing software supply chains, and building and publishing Python distributions.

Learning Objectives

After completing this chapter, students should be able to:

  • create reusable Python modules with intentional public interfaces;
  • explain namespaces, module search paths, import caching, and the role of `__name__`;
  • design regular packages using absolute and relative imports;
  • identify and resolve circular imports and module-shadowing problems;
  • create and use virtual environments;
  • distinguish abstract dependency declarations from reproducible environment locks;
  • manage application configuration without embedding secrets in source code;
  • define a contemporary project using `pyproject.toml`;
  • test an installed package rather than relying accidentally on the working directory;
  • build source and wheel distributions and publish them responsibly; and
  • apply dependency auditing, provenance, licensing, and release best practices.

1. Modules as Units of Reuse

A module is a Python source file whose definitions can be imported by another program. Suppose an AI application repeatedly calculates classification accuracy. The reusable operation can be placed in `mlmetrics.py`:

```python

mlmetrics.py

def accuracy(correct: int, total: int) -> float:

"""Return the fraction of predictions that are correct."""

if total <= 0:

raise ValueError("total must be positive")

if not 0 <= correct <= total:

raise ValueError("correct must be between 0 and total")

return correct / total

```

A separate program imports the module:

```python

import mlmetrics

score = mlmetrics.accuracy(87, 100)

print(score) # 0.87

```

The filename deliberately avoids names such as `statistics.py`, `random.py`, or `json.py`. Giving a local file the same name as an installed or standard-library module can shadow the intended module. For example, a local `json.py` may cause `import json` to load the local file rather than Python's JSON library.

1.1 Import forms

Python supports several import forms:

```python

import mlmetrics

from mlmetrics import accuracy

import mlmetrics as mm

from mlmetrics import accuracy as classification_accuracy

```

`import mlmetrics` is often clearest because `mlmetrics.accuracy` shows where the function originated. Aliases are appropriate when a conventional abbreviation exists or when they resolve a genuine naming conflict.

Wildcard imports such as `from mlmetrics import *` are discouraged. They obscure the origin of names, make collisions more likely, and reduce support from static-analysis tools.

1.2 Public interfaces

A module should expose a small, intentional interface. A leading underscore conventionally marks an implementation detail:

```python

__all__ = ["accuracy"]

def _validate_count(value: int) -> None:

if value < 0:

raise ValueError("count cannot be negative")

```

`__all__` controls the names imported by `from module import *`; it does not provide security or make other names inaccessible. Documentation and naming conventions remain the primary means of communicating the supported interface.

A strong module normally has:

  • one coherent responsibility;
  • documented functions and classes;
  • validation at public boundaries;
  • minimal work at import time;
  • no embedded credentials or machine-specific paths; and
  • tests independent of interactive execution.

2. Namespaces and Import Semantics

A namespace maps identifiers to objects. Python commonly uses local, enclosing, global, and built-in namespaces—the LEGB lookup order. Every imported module also has its own global namespace.

```python

thresholds.py

DEFAULT_THRESHOLD = 0.5

def is_positive(probability: float) -> bool:

return probability >= DEFAULT_THRESHOLD

```

`DEFAULT_THRESHOLD` and `is_positive` belong to the namespace of `thresholds`. Accessing them as `thresholds.DEFAULT_THRESHOLD` avoids confusing them with similarly named values elsewhere.

2.1 What happens during an import

At a high level, `import package.module` causes Python to:

1. check `sys.modules` for an already loaded module;

2. ask import finders to locate a matching module specification;

3. create a module object;

4. place it in `sys.modules` before fully executing its code;

5. execute the module's top-level statements; and

6. bind the imported name in the importing namespace.

The early insertion into `sys.modules` helps recursion handling, but it also explains why circular imports may observe a partially initialized module.

Modules are normally executed only on their first import within a process. Later imports reuse the cached object. `importlib.reload()` exists, but production code should not depend on reloading because references imported elsewhere may still point to old objects.

2.2 The module search path

Import finders commonly search locations represented by `sys.path`. Its contents are influenced by the entry-point location, the Python installation, the active environment, installed packages, and potentially the `PYTHONPATH` environment variable.

```python

import importlib.util

spec = importlib.util.find_spec("ai_metrics")

print(spec.origin if spec else "not found")

```

For debugging, inspecting `module.__file__` or `find_spec()` is safer than guessing which file was imported. Modifying `sys.path` inside application code is usually a design smell. Installing the project into an isolated environment is more reliable.

2.3 `__name__` and executable modules

Each module receives a `__name__` attribute. When imported, its value is normally the qualified module name. When a source file is executed directly, its `__name__` is set to `"__main__"`.

```python

demo_metrics.py

from mlmetrics import accuracy

def main() -> None:

print(accuracy(18, 20))

if __name__ == "__main__":

main()

```

Importing `demo_metrics` defines `main` but does not run it. Executing `python demo_metrics.py` runs `main`. This guard prevents demonstrations or command-line behavior from occurring merely because another module imported the file.

For packaged applications, a console-script entry point is generally preferable to asking users to execute an internal file directly.

2.4 Import-time side effects

Top-level code runs during import. Expensive model loading, network requests, database writes, and argument parsing should therefore not occur unconditionally at module scope. Such effects slow startup, complicate testing, and can make imports fail for unrelated reasons. Constants and lightweight object definitions are appropriate; operational behavior belongs in functions or explicit entry points.

3. From Modules to Packages

A regular package is normally a directory containing an `__init__.py` file and one or more modules or subpackages. A professional project often uses a `src` layout:

```text

ai-metrics-project/

├── pyproject.toml

├── README.md

├── LICENSE

├── src/

│ └── ai_metrics/

│ ├── __init__.py

│ ├── classification.py

│ ├── validation.py

│ └── cli.py

└── tests/

└── test_classification.py

```

The distribution name used by an installer may contain a hyphen, such as `ai-metrics`. The import package is commonly written with an underscore, such as `ai_metrics`. These are related but distinct concepts.

The `src` layout prevents the repository root from accidentally making the package importable. Tests are therefore more likely to exercise the installed package, revealing missing files and packaging errors earlier.

3.1 `__init__.py` and package interfaces

`__init__.py` identifies a regular package and may expose selected names:

```python

src/ai_metrics/__init__.py

from .classification import accuracy

__all__ = ["accuracy"]

```

Users can then write:

```python

from ai_metrics import accuracy

```

An `__init__.py` file should generally remain lightweight. Importing many submodules from it can increase startup time and contribute to circular dependencies.

Python also supports namespace packages, which may omit `__init__.py` and allow one logical package to span multiple installed distributions. They are useful for large organizational ecosystems, but regular packages are simpler for most single-project applications.

3.2 Absolute and relative imports

Inside a package, an absolute import begins from the package's top-level name:

```python

from ai_metrics.validation import validate_counts

```

An explicit relative import begins from the current package:

```python

from .validation import validate_counts

```

One dot means the current package; two dots mean its parent. Relative imports are permitted only when Python knows the module's package context. Consequently, directly running an internal file such as:

```bash

python src/ai_metrics/classification.py

```

may cause a relative import to fail. Run it as a module from a suitable environment instead:

```bash

python -m ai_metrics.classification

```

For public clarity, absolute imports are often preferred. Explicit relative imports can be concise for tightly coupled modules within the same package. Implicit relative imports are not used in Python 3.

3.3 Circular imports

A circular import occurs when module A imports module B while B directly or indirectly imports A. Consider:

```python

report.py

from .model import Model

model.py

from .report import create_report

```

Because a module is inserted into `sys.modules` before execution finishes, the second module may encounter the first before `Model` has been defined. Typical symptoms include an import error referring to a partially initialized module or a missing attribute.

Better remedies address design rather than import order:

1. move shared abstractions into a third module, such as `core.py`;

2. apply dependency inversion so both modules depend on an interface;

3. pass collaborators as function arguments;

4. use `typing.TYPE_CHECKING` for imports required only by type checkers; or

5. as a limited fallback, delay an import inside a function.

```python

from typing import TYPE_CHECKING

if TYPE_CHECKING:

from .model import Model

def create_report(model: "Model") -> str:

return model.name

```

Postponed or string annotations can avoid runtime imports used solely for typing. Local imports may break a cycle, but overuse can conceal poor architecture.

4. Virtual Environments

An installed package affects the Python interpreter that loads it. Installing every project into one global interpreter creates version conflicts and weakens reproducibility. A virtual environment provides an isolated Python executable and package installation directory.

Create an environment in a project directory:

```bash

python -m venv .venv

```

Activate it on POSIX shells:

```bash

source .venv/bin/activate

```

Activate it in Windows PowerShell:

```powershell

.venv\Scripts\Activate.ps1

```

Then verify the selected interpreter:

```bash

python -c "import sys; print(sys.executable)"

python -m pip --version

```

Using `python -m pip` ties the installer explicitly to the currently selected interpreter. Activation is convenient but not mandatory; the environment's Python executable can be invoked directly.

Install a project in editable mode during development:

```bash

python -m pip install -e .

```

An editable installation makes source changes available without rebuilding after each edit while still using package metadata. It should not replace testing the final wheel.

Virtual environments should not normally be committed to version control. They contain platform-specific paths and can be recreated from project metadata and lock information.

4.1 What isolation does not guarantee

A virtual environment isolates Python-level installations, but it does not automatically isolate:

  • operating-system libraries;
  • GPU drivers or CUDA runtimes;
  • external services and databases;
  • environment variables;
  • hardware differences; or
  • nondeterministic algorithms.

AI systems may therefore require containers, platform-specific lock files, documented accelerator versions, random seeds, and model or dataset provenance in addition to a virtual environment.

5. Dependency Management and Reproducibility

A dependency is an external package required by the project. Dependency management has two related but distinct goals.

An abstract project declaration states acceptable versions, for example:

```text

numpy>=1.26,<3

```

A lock or fully resolved environment records exact versions selected for a particular context, including transitive dependencies. Libraries usually need compatible version ranges so that they can coexist with other packages. Deployable applications often need stricter locking for reproducibility.

5.1 Version specifiers

Common specifiers include:

  • `requests>=2.31`: at least version 2.31;
  • `numpy>=1.26,<3`: a bounded range;
  • `package==1.4.2`: exactly one version; and
  • `tool~=2.1`: compatible release, generally at least 2.1 but below 3.0.

Exact pins alone do not guarantee identical behavior across platforms because installers may select different artifacts or platform-specific transitive dependencies. Stronger reproducibility may require a lock tool that records artifact hashes, Python versions, and platform markers.

5.2 Locking workflow

A contemporary workflow separates declared dependencies from generated lock data:

1. declare direct dependencies and Python requirements in `pyproject.toml`;

2. use a resolver or environment-management tool to generate a lock;

3. commit the lock when reproducible application or development environments are required;

4. install from the lock in continuous integration and deployment; and

5. update deliberately, rerun tests, and review security and compatibility changes.

Tools such as `uv`, Poetry, PDM, and pip-tools provide different locking workflows. Their lock formats and cross-platform guarantees differ, so teams must document the selected tool and version. A plain command such as `pip freeze > requirements.txt` records the current environment, including transitive and possibly unrelated packages. It can create a deployment snapshot, but it is not a substitute for carefully declared project metadata.

5.3 Optional dependencies

Development tools should not burden ordinary users. Optional dependency groups can separate testing or documentation requirements:

```toml

[project.optional-dependencies]

test = ["pytest>=8,<9"]

dev = ["build>=1,<2", "twine>=5,<7", "pip-audit>=2.7,<3"]

```

They may be installed with:

```bash

python -m pip install -e ".[test,dev]"

```

5.4 Auditing and maintenance

Dependencies create security, licensing, availability, and supply-chain risks. A typical audit command is:

```bash

python -m pip_audit

```

An audit compares installed or resolved packages with known vulnerability data. A clean report does not prove that software is secure: vulnerabilities may be undiscovered, advisories may be incomplete, and malicious behavior need not have a vulnerability identifier.

Professional maintenance also includes:

  • minimizing unnecessary dependencies;
  • reviewing package ownership and release history;
  • verifying names to avoid typosquatting;
  • using trusted indexes and protected publishing credentials;
  • reviewing licenses for compatibility;
  • updating dependencies in controlled changes; and
  • generating a software bill of materials when organizational policy requires it.

6. Configuration Management

Configuration varies by deployment; code expresses stable behavior. Examples include service URLs, log levels, model paths, feature flags, and database connection strings.

A simple application can read environment variables:

```python

import os

def load_config() -> dict[str, object]:

endpoint = os.environ.get("MODEL_ENDPOINT", "http://localhost:8000")

timeout = float(os.environ.get("REQUEST_TIMEOUT_SECONDS", "5.0"))

if timeout <= 0:

raise ValueError("REQUEST_TIMEOUT_SECONDS must be positive")

return {"endpoint": endpoint, "timeout_seconds": timeout}

```

Configuration should be validated early and should produce useful error messages. For larger systems, a typed settings class can centralize parsing and validation.

A `.env` file may be convenient for local development, but Python does not read it automatically. A library or tool must load it. Real secret-bearing `.env` files should usually be excluded from version control; a `.env.example` containing placeholder names may be committed instead.

A practical precedence policy might be:

1. command-line arguments;

2. environment variables;

3. project or user configuration files;

4. documented defaults.

The exact order is a design decision and must be documented. Secrets should be supplied through environment controls or a dedicated secret manager, not hard-coded in modules, committed in configuration files, printed in logs, or included in built distributions.

7. Project Metadata with `pyproject.toml`

`pyproject.toml` is the central configuration file for modern Python packaging. It declares the build system and can contain standardized project metadata under PEP 621 as well as tool-specific configuration.

A minimal example is:

```toml

[build-system]

requires = ["hatchling>=1.24,<2"]

build-backend = "hatchling.build"

[project]

name = "ai-metrics"

version = "0.1.0"

description = "Validated metrics for teaching classification workflows"

readme = "README.md"

requires-python = ">=3.11"

license = { text = "MIT" }

authors = [

{ name = "Example Development Team" }

]

dependencies = []

[project.optional-dependencies]

test = ["pytest>=8,<9"]

dev = ["build>=1,<2", "twine>=5,<7", "pip-audit>=2.7,<3"]

[project.scripts]

ai-metrics = "ai_metrics.cli:main"

[tool.pytest.ini_options]

testpaths = ["tests"]

```

The `[build-system]` table tells a PEP 517 frontend which backend and build requirements to use. The `[project]` table describes the distribution. The script declaration creates an `ai-metrics` command that calls `main` in `ai_metrics.cli` after installation.

Different backends—such as Hatchling, setuptools, Flit, or a tool-specific backend—have different configuration details. A project should select one intentionally rather than mixing incompatible instructions.

For larger projects, versioning may be static, dynamically derived from source control, or read from a designated module. There should be one authoritative version source to avoid inconsistent metadata.

8. Testing a Package

A package test should import the package through the same interface used by consumers. For the metrics project:

```python

tests/test_classification.py

import pytest

from ai_metrics import accuracy

def test_accuracy() -> None:

assert accuracy(18, 20) == pytest.approx(0.9)

def test_zero_total_is_rejected() -> None:

with pytest.raises(ValueError, match="positive"):

accuracy(0, 0)

```

Run tests in the isolated environment:

```bash

python -m pytest

```

Testing should include normal behavior, boundary conditions, invalid inputs, public imports, and supported Python versions. For an AI package, tests may additionally verify array shapes, data types, deterministic behavior under stated seeds, and tolerance-based floating-point results.

A robust release process tests both the source tree and the built wheel. Installing the wheel into a clean environment can reveal omitted data files, undeclared dependencies, and assumptions hidden by an editable installation.

Static type checking, linting, formatting, and documentation checks complement tests but do not replace them. Continuous integration should recreate environments from declared metadata rather than rely on a developer's existing machine.

9. Building and Distributing a Package

A Python project is commonly distributed in two formats:

  • a source distribution (`sdist`), usually a `.tar.gz` archive containing source and metadata; and
  • a wheel, a `.whl` archive designed for installation without running the project's build process on the target machine.

For pure Python projects, a wheel is often platform-independent. Packages containing compiled extensions may require separate wheels for operating systems, CPU architectures, Python implementations, or ABI versions.

Install the build frontend and build the project:

```bash

python -m pip install build

python -m build

```

The `dist/` directory should then contain an sdist and a wheel. Validate metadata and archive structure:

```bash

python -m twine check dist/*

```

Before publishing, test the wheel in a fresh environment:

```bash

python -m venv wheel-test

wheel-test/bin/python -m pip install dist/ai_metrics-0.1.0-py3-none-any.whl

wheel-test/bin/python -c "from ai_metrics import accuracy; print(accuracy(9, 10))"

```

On Windows, use the corresponding `wheel-test\Scripts\python.exe` path.

A cautious public-release workflow first uploads to TestPyPI:

```bash

python -m twine upload --repository testpypi dist/*

```

After verification, an authorized maintainer may publish to the production package index. Public versions generally cannot be replaced safely after publication; corrections normally require a new version. API tokens should be stored using secure platform mechanisms, not embedded in commands, files committed to source control, or CI logs.

Distribution also creates governance duties. Maintainers should provide a license, accurate metadata, release notes, compatibility information, vulnerability-reporting guidance, and a deprecation policy. Publishing generated or AI-assisted code does not remove the obligation to verify correctness, provenance, security, and license compliance.

10. Applied Case: Packaging an Inference Client

The following is an illustrative case.

Background

A university AI laboratory has three applications that call the same remote inference service. Each application contains copied code for request validation, endpoint configuration, and response parsing.

Problem

The copies have diverged. One application uses a five-second timeout, another waits indefinitely, and a third contains a committed API token. Developers install dependencies globally, causing one application to fail after an unrelated package upgrade. Tests import directly from repository folders, so missing package metadata remains undetected.

Analysis

The laboratory faces several distinct problems:

  • duplicated functionality lacks a stable public interface;
  • secrets and deployment settings are mixed with code;
  • environments are not isolated or reproducible;
  • tests do not represent consumer installation;
  • dependencies are unaudited; and
  • there is no controlled release artifact.

Approach and solution

The team creates a distribution named `lab-inference-client` with the import package `lab_inference`. It uses a `src` layout with `client.py`, `config.py`, `errors.py`, and a lightweight `__init__.py`. Shared protocol types are placed in `protocol.py`, preventing a circular dependency between request and response modules.

The endpoint and timeout are read from validated environment variables. The token is supplied by the institution's secret manager and is never logged. Runtime dependencies are declared as bounded ranges in `pyproject.toml`; testing and auditing tools are optional development dependencies.

Each developer creates `.venv`, installs the package in editable mode, and runs unit tests. Continuous integration installs from the team's lock data, audits known vulnerabilities, builds both distribution formats, installs the wheel into a clean environment, and runs an integration smoke test against a non-production service.

The first release is uploaded to a private institutional index. Access tokens use least privilege, and each release receives immutable version metadata and release notes.

Outcome

All applications now consume one documented API. Configuration differences are explicit, the secret is removed from source history through the institution's incident process, and dependency updates are tested centrally. Installing the built wheel exposes an omitted package data file before release, demonstrating why source-tree tests alone were insufficient.

Learning points

Modularity, packaging, environment isolation, configuration, testing, and supply-chain controls solve different parts of the same maintainability problem. A reusable package is not merely a directory of Python files; it is a versioned, tested, installable, and governed software product.

11. Practical Laboratory Procedures

Laboratory 1: Diagnose import shadowing

1. Create a temporary directory containing a file named `json.py`.

2. From that directory, run `python -c "import json; print(json.__file__)"`.

3. Rename the file to `json_helpers.py` and remove any generated `__pycache__` entry associated with the old module.

4. Run the command again and explain the change using the module search path.

Perform this experiment only in a disposable directory.

Laboratory 2: Create and test a package

1. Create the `src/ai_metrics` and `tests` structure shown earlier.

2. Implement `accuracy` with input validation.

3. expose it through `ai_metrics/__init__.py`;

4. create a virtual environment and install the project with `python -m pip install -e ".[test]"`;

5. write tests for valid input, zero totals, negative counts, and `correct > total`; and

6. run tests from a directory other than the repository root to confirm that installation works.

Laboratory 3: Build verification

1. Add valid `pyproject.toml`, `README.md`, and license files.

2. Build an sdist and wheel with `python -m build`.

3. inspect the artifacts with `python -m twine check dist/*`;

4. create a fresh environment and install only the wheel;

5. execute a smoke test through the public API; and

6. compare the installed files with the intended package contents.

Laboratory 4: Dependency and configuration review

1. Add one justified runtime dependency and place testing tools in an optional group.

2. Generate lock data with the dependency tool selected by the course or organization.

3. Recreate the environment from that lock.

4. Run a vulnerability audit and document any finding, affected version, proposed remediation, and residual risk.

5. Add configuration for an endpoint and positive timeout through environment variables.

6. Verify that no secret, `.venv` directory, build artifact, or real `.env` file is tracked by version control.

12. Challenges, Limitations, and Best Practices

Python packaging is standardized at several interfaces, but tools still vary in commands, lock formats, and behavior. Platform-specific numerical libraries and GPU stacks can make a single universal lock impractical. Dependency resolvers select versions that satisfy declared constraints; they cannot prove semantic compatibility or scientific validity.

Import flexibility can also hide architectural problems. Mutating `sys.path`, relying on the current directory, performing heavy work during import, and resolving every cycle with local imports make systems fragile. Package boundaries should follow coherent responsibilities rather than arbitrary file sizes.

Recommended professional practices are:

  • use descriptive module names that do not shadow standard or third-party packages;
  • expose a small and documented public API;
  • prefer a `src` layout for distributable projects;
  • keep imports and `__init__.py` files lightweight;
  • isolate each project in a virtual environment;
  • declare direct dependencies and maintain intentional lock data;
  • test supported environments in continuous integration;
  • validate configuration and keep secrets outside source and distributions;
  • audit dependencies while recognizing the limits of vulnerability databases;
  • build and test wheels in clean environments;
  • publish with protected credentials and immutable versioning; and
  • document licenses, provenance, compatibility, and release policy.

Chapter Summary

Modules divide Python programs into reusable namespaces, while packages organize related modules and define stable public interfaces. Python imports locate, initialize, execute, and cache modules; understanding `sys.path`, `sys.modules`, `__name__`, and import-time side effects is therefore essential. Absolute and explicit relative imports support package structure, while circular imports commonly indicate that responsibilities or dependencies should be redesigned.

Virtual environments isolate Python installations, although external libraries, hardware, and nondeterminism require additional controls. Modern projects declare build requirements and metadata in `pyproject.toml`, separate runtime from optional development dependencies, and use deliberate locking when reproducibility is required. Configuration must be validated and separated from secrets.

Professional distribution involves testing the installed public interface, building both source and wheel artifacts, verifying them in clean environments, and publishing through controlled credentials. Dependency auditing, licensing, provenance, and release governance are integral to reliable AI and software engineering rather than optional administrative tasks.

Key Terms

Module: A Python file containing importable definitions and executable statements.

Package: A structured namespace that organizes related Python modules and subpackages.

Distribution package: An installable project artifact identified by package metadata, distinct from an import package.

Namespace: A mapping from identifiers to Python objects.

Import path: The set of locations and import mechanisms Python uses to locate modules.

`sys.modules`: The process-level cache of modules that have already begun or completed loading.

Import shadowing: Accidental loading of a local module whose name conflicts with an intended module.

`__name__`: A module attribute containing its qualified name or `"__main__"` when executed as the main module.

Absolute import: An import expressed from the top-level package namespace.

Relative import: An explicit import expressed relative to the current package using leading dots.

Circular import: A dependency cycle in which modules import one another before initialization is complete.

Virtual environment: An isolated Python interpreter context with its own installed packages.

Dependency lock: Resolved version and artifact information used to recreate a controlled environment.

Transitive dependency: A package required indirectly through another dependency.

`pyproject.toml`: The standard project file for build-system declarations, metadata, and tool configuration.

Build backend: A tool that converts project source into distribution artifacts through standardized build interfaces.

Source distribution: An archive containing source and metadata from which a project can be built.

Wheel: A built distribution format intended for efficient installation.

Editable installation: A development installation that exposes changes in the source tree without repeated rebuilding.

Dependency audit: An assessment of dependencies against known vulnerabilities and related supply-chain risks.

Review Questions

Short Answer Questions

1. What is the difference between a module and a package?

2. Why can naming a local file `json.py` cause an import problem?

3. What value does `__name__` receive when a file is executed as the main program?

4. Why is `python -m pip` often preferable to invoking `pip` directly?

5. Distinguish a wheel from a source distribution.

Long Answer Questions

1. Explain Python's import process, including the roles of finders, `sys.modules`, module execution, and import caching.

2. Compare absolute and relative imports, identifying suitable use cases and limitations for each.

3. Explain how virtual environments, dependency declarations, and lock data contribute differently to reproducibility.

4. Design a secure configuration strategy for an AI inference client, including validation, precedence, and secret handling.

5. Describe a complete workflow for testing, building, verifying, and publishing a Python distribution.

Analytical / Application Questions

1. Two package modules import each other and fail with a partially initialized module error. Analyze at least three possible redesigns and justify the most maintainable one.

2. An AI application works in an editable installation but fails after its wheel is installed in a clean environment. Develop a diagnostic procedure covering package discovery, missing files, undeclared dependencies, and import assumptions.

3. A team pins every package using `pip freeze`, commits its virtual environment, and stores an API key in `.env` under version control. Evaluate each practice and propose a secure, reproducible replacement workflow.

Multiple Choice Questions with Answers

Question 1

Which statement best describes a Python module?

A. A Python source file that can define reusable importable objects

B. An isolated interpreter containing installed packages

C. A built archive that must contain compiled machine code

D. A server that hosts Python distributions

Correct Answer: A. A Python source file that can define reusable importable objects

Question 2

What is the principal risk of naming a project file `json.py`?

A. Python will refuse to compile every file in the project

B. The local file may shadow the standard-library `json` module

C. The file will automatically become a namespace package

D. The file will be excluded from wheel distributions

Correct Answer: B. The local file may shadow the standard-library `json` module

Question 3

What happens to guarded code under `if __name__ == "__main__":` when its file is imported normally?

A. It runs before all import statements

B. It runs in a separate virtual environment

C. It does not run because `__name__` is the module's import name

D. It is removed permanently from the module object

Correct Answer: C. It does not run because `__name__` is the module's import name

Question 4

Why can a circular import expose a partially initialized module?

A. Python places a module in `sys.modules` before finishing its execution

B. Relative imports always run in separate processes

C. Every import clears the module cache

D. Python compiles only one function from each module

Correct Answer: A. Python places a module in `sys.modules` before finishing its execution

Question 5

Which command creates a virtual environment named `.venv`?

A. python -m build .venv

B. python -m pip .venv

C. python -m venv .venv

D. python -m twine .venv

Correct Answer: C. python -m venv .venv

Question 6

Which statement correctly distinguishes dependency declarations from lock data?

A. Declarations specify acceptable requirements; lock data records resolved versions for reproducibility

B. Declarations are used only for tests; lock data is used only for documentation

C. Declarations contain secrets; lock data removes them

D. Declarations build wheels; lock data uploads them

Correct Answer: A. Declarations specify acceptable requirements; lock data records resolved versions for reproducibility

Question 7

Where are standardized modern Python project metadata and build-system requirements commonly declared?

A. README.md

B. pyproject.toml

C. __pycache__

D. sys.modules

Correct Answer: B. pyproject.toml

Question 8

Which practice is most appropriate for an application secret?

A. Store it as a constant in `__init__.py`

B. Include it in the wheel so installation is automatic

C. Commit it in a real `.env` file

D. Supply it through controlled environment or secret-management facilities

Correct Answer: D. Supply it through controlled environment or secret-management facilities

Question 9

Why should a built wheel be tested in a clean environment?

A. To detect missing files, undeclared dependencies, and source-tree assumptions

B. To convert all relative imports into absolute imports

C. To guarantee that no future vulnerability will be discovered

D. To make virtual environments unnecessary

Correct Answer: A. To detect missing files, undeclared dependencies, and source-tree assumptions

Question 10

Which distribution format is designed for installation without running the project's build process on the target machine?

A. A source distribution

B. A wheel

C. A `.env` file

D. A dependency audit report

Correct Answer: B. A wheel

Programming in Python
Chapter 9 of 15