🐍 Python Course · Stage 4 · Lesson 70 of 89 · Concept: Modules
Course Overview →

🟩 These concept pages connect Python to programming in general. Read Beginner tab first.

← Concept: Error Handling Concept: Concurrency →
thecodex.expert  ·  The Codex Family of Knowledge
CS Concepts

Modules

Every non-trivial program is split across multiple files. Modules are the mechanism that lets those files find and use each other.

CS Concepts Language-independent ~18 min read Last verified:
Canonical Definition

A module is a self-contained unit of source code that defines a namespace — a collection of names (functions, classes, variables) that can be selectively imported by other parts of a program — enabling code organisation, reuse across files, and isolation of implementation details behind a defined public interface.

🟩 Beginner

Organising code into files and namespaces

What you'll learn: What the module problem is (how to organise large programs), how importing works, and the difference between modules and packages.

How to read this tab: The Python-specific import system is covered in depth at Modules & Packages. This page explains why modules exist and how they work conceptually.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: Functions + Scope
One sentence that captures it

A module is a file of code with a name — you write code in it, and other files can import and use whatever that module makes available.

Without modules, every program would be one giant file. All function names would need to be globally unique. Any change to shared code would require checking every function in the codebase. Modules solve this by creating independent namespaces — each module has its own scope, its own names. Two modules can both have a function named "process" without conflict.

The problem modules solve

Without modules, all code in a program shares one global namespace. A function called format in your code would conflict with Python's built-in format. Modules give each file its own private namespace — its own isolated collection of names — so code can grow across files without collisions.

💡

Three import styles — each has its place: import json — explicit namespace, clear where json.loads() comes from. from json import loads — shorter but less clear origin. from json import loads as parse — alias for readability or conflict resolution. Avoid from module import * — it pollutes your namespace and makes code hard to read.

Importing from modules

Pythonimports.py
import math                  # whole module
print(math.sqrt(16))         # 4.0 — access via name

from math import sqrt, pi    # specific names
print(sqrt(16))              # 4.0

from math import sqrt as sq  # alias
print(sq(25))                # 5.0

# Avoid: from math import *  (pollutes namespace)
💡

A package is just a directory with an __init__.py file. The __init__.py marks the directory as importable and can expose a clean public API. If store/__init__.py contains from .cart import add_item, users can write from store import add_item instead of from store.cart import add_item.

Packages: directories of modules

A package is a directory of module files allowing hierarchical organisation. In Python, a directory with __init__.py is a package. Package managers (pip, npm, cargo, Maven) handle downloading and installing external packages.

📎

Python's privacy is convention-based. Names starting with _ are "private by convention" — don't import them from outside the module. Names listed in __all__ are the module's public API. from module import * only imports names in __all__ (if defined). This system relies on discipline, not enforcement — Python trusts you to respect the convention.

Public vs. private

Modules control which names they expose. Python: names starting with _ are private by convention. Rust: everything private by default; pub makes it public. JavaScript ES modules: only exported names are visible.

JavaScript (ES Modules)math_utils.js
const PI = 3.14159;           // private — not exported
export function square(x) { return x * x; }
export const TAX_RATE = 0.18;

// In app.js:
// import { square, TAX_RATE } from './math_utils.js';
// PI is not accessible — not exported

✅ Beginner tab complete — check your understanding

  • I can explain why modules exist: prevent name collisions, enable code reuse, make large programs manageable
  • I know what a namespace is: a container that maps names to values
  • I know the difference between a module (one file) and a package (directory of files)

Continue to Concurrency →

🔵 Intermediate

Python's import system, namespace packages, and circular imports

What you'll learn: How Python's import system searches for modules (sys.path). Namespace packages without __init__.py. How circular imports happen and three ways to fix them.

How to read this tab: Circular imports are a common stumbling block in larger Python projects. Read this section when you hit your first ImportError due to circular dependencies.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: Beginner tab
📎

The import system has three stages: 1) Check sys.modules (the cache) — if already imported, return the cached module. 2) Find the module using finders (built-in, frozen, path-based). 3) Load the module (compile to bytecode, execute the module code, store in sys.modules). Understanding stage 1 explains why importing the same module twice doesn't re-execute it.

Python's import system in detail

On import math, Python searches sys.path (current dir, stdlib, installed packages), compiles to bytecode (cached in __pycache__), executes the module's top-level code, stores in sys.modules. Subsequent imports return the cached object — module code runs exactly once.

Namespace packages (Python 3.3+)

PEP 420: directories without __init__.py can act as namespace packages, allowing a single logical package to span multiple directories — useful for distributed large projects.

Module systems across languages

LanguageMechanismPackage managerVisibility
Python.py files; __init__.py packagespip / PyPI_ convention; __all__
JavaScriptES modules (import/export)npm / yarnexport keyword
Javapackages (directory structure)Maven / Gradlepublic/private/protected
Rustmod keyword; cratescargo / crates.iopub; private by default
Gopackages (dir = package)go modules (go.mod)Capitalised = public
CHeader files (.h)none built-instatic (file-local) / extern

Circular imports happen when A imports B and B imports A. When Python starts importing A, it starts executing A's code. When A does import B, Python starts importing B. B tries to import A — but A is still being imported (not finished yet). Python gives B a partially-initialised A module, which is missing the names defined after the import B line. Fix: move the import inside the function that needs it.

Circular imports

Module A imports B, B imports A. Python allows this but B gets A's partially-initialised module — names not yet defined at the import point are missing. Solution: restructure dependency as one-way, or use deferred imports (import inside a function).

Commonly confused
A module and a package are not the same. A module is a single file. A package is a directory of modules. Both are accessed with import, but their structure differs.
from module import * is not recommended. Wildcard imports bring all exported names into the current namespace, risking silent overwrites when two modules export the same name.
Importing a module does not re-execute it. Python caches modules in sys.modules. A second import math returns the same object. Module-level code (assignments, print statements) runs once — on first import.
How this connects
Requires first
Enables
Confused with

✅ Intermediate tab complete — check your understanding

  • I know Python's import search order: built-ins → sys.path (script dir → PYTHONPATH → site-packages)
  • I can diagnose a circular import (A imports B, B imports A)
  • I know three ways to fix circular imports: restructure, defer the import, or import the module not the names

Continue to Concurrency →

🔴 Expert

Module systems across languages and C linkage

What you'll learn: How module systems differ across Python, JavaScript (ES modules), Java (packages), Go, and Rust. C's compilation units and linkage model. Python's __name__ and module execution.

How to read this tab: Read when studying multiple languages or working with C libraries from Python (ctypes, cffi).

⏱ 20 min 📄 3 sections 🔶 Prerequisite: After Stage 2

Compilation units and linkage in C

In C, each .c file is a translation unit compiled independently. Names with external linkage (default for functions and globals) are visible across translation units via the linker. Names with internal linkage (static) are file-local. Header files (.h) declare interfaces; .c files implement them. C++20 introduced first-class modules (import/export module) as a more modern alternative.

Python's __name__ and module execution

A Python module's top-level code executes when first imported. The special variable __name__ is set to the module's name for imports, and to "__main__" when run directly. The idiom if __name__ == "__main__": main() prevents entry-point code from running on import — essential for reusable modules.

Specification reference

Python: Language Reference §5 — The import system. Rust: The Rust Book ch07. JavaScript: ECMAScript 2024 §16. Go modules: go.dev/ref/mod.

✅ Expert tab complete

  • I know JavaScript has two module systems: CommonJS (require) and ES modules (import)
  • I know what compilation units and linkage mean in C/C++
  • I understand why if __name__ == "__main__" works and when to use it

Continue to Concurrency →

Sources

1
Python Software Foundation. Python Language Reference, §5 — The import system. docs.python.org/3/reference/import.html.
2
The Rust Book, Chapter 7. doc.rust-lang.org/book/ch07-00-managing-growing-projects.html.
3
ECMAScript 2024, §16 — Scripts and Modules. Ecma International.
4
Go Modules Reference. go.dev/ref/mod.
Source confidence: High Last verified: Primary source: Python Language Reference §5 — The import system