šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Packages vs Modules

The precise difference between a module and a package, what __init__.py actually does, and how to design clean, non-circular import structures as a codebase grows.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What specifically makes a directory an importable Python package (in the traditional sense)?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

"Module" and "package" get used interchangeably in casual conversation, but they mean specific, different things in Python, and confusing them leads to real structural mistakes as a codebase grows. This lesson defines both precisely and covers the import design patterns that keep large codebases maintainable.

1The Precise Distinction

A module is any single .py file — math_utils.py is a module. Importing it (import math_utils) executes the file's top-level code exactly once per process (subsequent imports return the same cached module object from sys.modules, they don't re-run the file), and binds the names defined at that top level (functions, classes, variables) as attributes accessible via math_utils.square.

A package is a directory of modules, traditionally marked as importable by containing an __init__.py file — mypackage/ with an __init__.py inside it is a package; mypackage/math_utils.py is a module that lives inside that package. from mypackage import math_utils imports the submodule; from mypackage.math_utils import square reaches directly for a name defined inside it.

The distinction matters for how you reason about a growing codebase: a module is a flat, single namespace; a package is a *hierarchy* of namespaces, letting you group related modules (mypackage/parsers/, mypackage/exporters/) without every function and class in the whole project sharing one flat, increasingly crowded namespace.

āœ•
—
+
# math_utils.py
def square(x: int) -> int:
    return x * x

# main.py
import math_utils
print(math_utils.square(4))  # 16
localhost:3000
Console Output
math_utils.square(4)
16

2__init__.py: More Than Just a Marker

An empty __init__.py is entirely valid and common — its mere presence is enough to mark the directory as a package. But __init__.py is also an ordinary Python module in its own right, executed once when the package is first imported, which makes it the natural place to define the package's public API: re-exporting selected names from internal submodules so callers can write from mypackage import square instead of needing to know it actually lives in mypackage/math_utils.py.

This re-export pattern — from .math_utils import square inside __init__.py — is a deliberate design choice about what's considered internal versus public. It lets you freely reorganize or rename internal submodules later (splitting math_utils.py into two files, for instance) without breaking every caller that imported square from the package's top level, as long as __init__.py is updated to still re-export it from wherever it now lives.

__all__, a list of strings assigned in __init__.py (or any module), controls what from mypackage import * actually imports — an increasingly rare pattern in modern code, but __all__ also serves as clear, explicit documentation of a module's intended public surface, which many linters and IDEs use to distinguish 'public API' from 'implementation detail you probably shouldn't import directly'.

āœ•
—
+
mypackage/
  __init__.py      # makes this a package
  math_utils.py     # a module inside the package
  string_utils.py

# Usage:
from mypackage import math_utils
from mypackage.string_utils import slugify
localhost:3000
Public API Design
from mypackage import square, slugify
Callers never need to know the internal submodule structure

3Avoiding Circular Imports by Design

A circular import occurs when module a imports something from module b, and module b, directly or indirectly, imports something from module a — Python's import system detects this and typically raises ImportError (or, more confusingly, succeeds but leaves one of the modules partially initialized, with some names missing) because it can't fully finish executing either module before the other needs something from it.

The underlying cause is almost always a genuine structural problem, not a syntax issue: two pieces of code have a mutual dependency on each other, which usually means they're not as cleanly separated as the file boundary suggests. The most durable fix is extracting whatever both modules actually need — shared constants, a shared base class, a shared utility function — into a third module that both a and b can depend on one-directionally, eliminating the cycle at the design level rather than working around it.

Less durable but sometimes pragmatic workarounds exist — moving an import inside a function body so it's deferred until call time, rather than evaluated at module-load time — but these should be treated as a temporary patch while a real refactor is planned, not a long-term pattern; they tend to make the actual dependency structure of a codebase harder to see just by reading the top of each file.

āœ•
—
+
# mypackage/__init__.py
from .math_utils import square
from .string_utils import slugify

__all__ = ["square", "slugify"]

# Now callers can do:
from mypackage import square, slugify  # instead of reaching into submodules
localhost:3000
Dependency Graph
a → shared ← b
One-directional dependencies eliminate the cycle entirely

4Step-by-Step Breakdown

Is utils.py a module or a package? What about the utils/ folder next to it? Let's get precise.

A module is simply a single .py file. Importing it runs the file top to bottom, once, and caches the result.

A package is a directory containing an __init__.py file — that file is what makes Python treat the directory as an importable package.

Checkpoint: What specifically makes a directory an importable Python package (in the traditional sense)?

  • →Containing an __init__.py file
  • →Being named lowercase with no underscores

__init__.py can be empty, or it can define what's exposed at the package's top level — controlling the public API.

Circular imports happen when two modules import from each other. Restructuring to a one-directional dependency avoids the entire problem.

Checkpoint: What is the most reliable fix for a circular import between two modules?

  • →Restructure the code so dependencies flow in one direction (e.g. extract shared code to a third module)
  • →Wrap the import in a try/except ImportError and ignore the failure

Once code is organized into modules and packages, pyproject.toml is where you formally declare what gets built and shipped from them.

Reproduce Real Import Caching. Finish cached_import(): sys.modules caches every imported module after its first import.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Design a package's __init__.py to define a clear, deliberate public API

Re-exporting selected names lets internal modules be reorganized freely later without breaking every caller — an empty __init__.py that forces callers to know your internal file layout is a missed opportunity for encapsulation.

Treat a circular import as a design smell, not a syntax problem to work around

Deferred/local imports can unblock you short-term, but the durable fix is almost always extracting the mutual dependency into a shared module both sides can depend on one-directionally.

Frequent Bugs

THE BUG

Reaching for a deferred (inside-function) import to silence a circular ImportError, without ever revisiting the underlying structural coupling that caused it.

THE FIX

Use a deferred import only as a temporary, explicitly-flagged workaround, and schedule the actual refactor: extract the mutually-needed code into a separate module both sides can import one-directionally.

Real-World Examples

Designing a Package Public API

An internal analytics package has grown to five submodules, and the team wants consumers to import from a stable, clean top-level API rather than reaching into internal files directly.

# analytics/__init__.py
from .events import track_event
from .metrics import compute_summary
from .exporters import export_to_csv

__all__ = ["track_event", "compute_summary", "export_to_csv"]

# Consumers write this:
from analytics import track_event
# not this:
from analytics.events import track_event

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Two modules importing from each other at the top level, causing an ImportError (or a partially-initialized module missing expected attributes) depending on which one is imported first.

# Wrong: circular dependency # a.py: from b import helper # b.py: from a import CONFIG # Correct: shared module breaks the cycle # shared.py: CONFIG = {...} # a.py: from shared import CONFIG # b.py: from shared import CONFIG # def helper(): ...

The Solution //

Extract the mutually-needed code (constants, base classes, shared functions) into a separate module both original modules import from one-directionally, removing the cycle.

Lesson Glossary

[01]Module

A single Python (.py) file, imported once per process and cached in sys.modules for subsequent imports.

Code Preview
// Module context

[02]Package

A directory of modules, traditionally marked importable by containing an __init__.py file.

Code Preview
// Package context

[03]__init__.py

A package's initialization module, run once on first import, commonly used to define the package's public re-exported API.

Code Preview
// __init__.py context

[04]Circular import

A situation where two or more modules depend on each other directly or indirectly, which the import system cannot fully resolve.

Code Preview
// Circular import context

Continue Learning