Python • Functions and Program Structure

Organizing Modules & Packages — Organize code into modules and packages (__init__.py concept)

Learn how to split your Python code into reusable modules and structured packages for cleaner, more maintainable programs.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 6 figures

SECTION 1

Historical Context & Motivation

When programming first began, entire programs lived inside a single file. As software grew larger and teams expanded, developers quickly realized that dumping thousands of lines of code into one file was a nightmare to read, debug, and maintain. The concept of modular programming emerged as a solution: break code into smaller, self-contained pieces that can be developed, tested, and reused independently.

Python embraced this philosophy from the very beginning. Guido van Rossum, Python's creator, designed the language with a built-in module system that makes organizing code straightforward. As Python's ecosystem grew, the package system was introduced to group related modules into directory hierarchies, giving developers a clean way to manage large projects.

1991 • Python 0.9 Released

Guido van Rossum releases the first version of Python, which already includes a module system with the import statement for loading external code files.

1994 • Python 1.0 & Standard Library

Python 1.0 ships with an expanding standard library of built-in modules like math and os, demonstrating the power of reusable code.

1997 • Packages Introduced

Python 1.5 formalizes the package system using directories with __init__.py files, enabling hierarchical module organization.

2012 • Namespace Packages (PEP 420 & 328)

Python 3.3 introduces namespace packages, allowing packages without an __init__.py file in certain advanced scenarios, though the traditional approach remains standard.

The central question this lesson addresses is: How do you organize Python code across multiple files and directories so that your projects stay clean, readable, and scalable? Understanding modules and packages is essential for any project that grows beyond a single script.

SECTION 2

Core Principles & Definitions

Before diving into code, let's define the key building blocks. In Python, a module is simply any file ending in .py. When you write import math, you are importing the math.py module from Python's standard library. A package is a directory (folder) that contains one or more modules, along with a special __init__.py file that tells Python "this folder is a package."

1

Module

A single .py file containing functions, classes, and variables. You can import it into other files using the import statement.
2

Package

A folder containing multiple modules and a special __init__.py file. Packages let you group related modules together under a shared namespace.
3

__init__.py

A file that marks a directory as a Python package. It can be empty or contain initialization code that runs when the package is first imported.
4

import Statement

The keyword used to load modules or specific objects from modules. Variants include import module, from module import func, and from package import module.
5

Namespace

A container that holds names (variables, functions) so they don't collide. Each module creates its own namespace, preventing naming conflicts between files.
✦

KEY TAKEAWAY

Think of a module like a single chapter in a textbook — it covers one topic. A package is the entire textbook, and __init__.py is the table of contents that tells Python what's inside. Without the table of contents, Python doesn't know the folder is a book worth reading.

SECTION 3

Visual Explanation — Project Structure

The diagram below shows a typical Python project structure. Notice how folders become packages when they contain an __init__.py file, and individual .py files serve as modules that can be imported anywhere in the project.

Python Project Directory Structure📁 my_project/📄 main.py📁 utils/← This is a PACKAGE📄 __init__.py← Marks folder as package📄 helpers.py← A module📄 formatting.py← Another module📁 models/← Another PACKAGE📄 __init__.py📄 user.py📄 product.py
A project with two packages (utils/ and models/). Each package has an __init__.py file plus one or more module files. The main.py file at the root imports from these packages.

In the diagram above, the blue folder icons represent packages, while individual .py files are modules. The __init__.py files act as gatekeepers — they signal to Python's import system that the directory should be treated as a package rather than just a random folder of files.

SECTION 4

How the Import System Works

When you write an import statement, Python follows a specific sequence to find and load the requested module. Understanding this mechanism helps you troubleshoot common errors like ModuleNotFoundError and structure your projects correctly.

The Import Search Process

  1. Step 1 — Check sys.modules cache: Python first looks in its cache of already-imported modules. If the module was loaded before, it reuses it immediately.
  2. Step 2 — Search sys.path: Python looks through a list of directories stored in sys.path. This includes the current directory, the standard library location, and any installed packages.
  3. Step 3 — Execute the module: Once found, Python runs all the code in the module file from top to bottom, creating the module's namespace with its functions, classes, and variables.
  4. Step 4 — Bind the name: The module object is bound to the name you used in the import statement, making it accessible in your code.

Import Syntax Variations

SyntaxWhat It DoesHow You Use It
import utils.helpersImports the entire module from the packageutils.helpers.clean(data)
from utils import helpersImports the module directly into current namespacehelpers.clean(data)
from utils.helpers import cleanImports a specific function from the moduleclean(data)
import utils.helpers as hImports the module with a shorter aliash.clean(data)

Common import patterns when working with packages

What __init__.py Actually Does

The __init__.py file runs automatically whenever the package is imported. It can be completely empty — its mere existence marks the directory as a package. However, you can also use it to define what gets exported when someone writes from package import * by setting the __all__ variable, or to run setup code like configuring logging or importing frequently used names for convenience.

⚠

Common Pitfall

If you forget to create __init__.py inside a folder, Python (before version 3.3) will not recognize that folder as a package, and your import statements will fail with a ModuleNotFoundError. Even in Python 3.3+, it is best practice to always include this file for clarity and compatibility.

SECTION 5

Inside __init__.py — Controlling Your Package

While an empty __init__.py is perfectly valid, putting code inside it unlocks powerful features. Let's explore the three most common uses: re-exporting names, defining __all__, and running initialization logic.

How __init__.py Connects Modules to the Outside Worldutils/ packagehelpers.pyformatting.py__init__.py__init__.py contentsfrom .helpers import cleanfrom .formatting import bold# Now users can write:from utils import cleanfrom utils import bold__all__ = ["clean", "bold"]main.pyfrom utils import cleanresult = clean(data)# Simple & clean! ✓❌ Without __init__.py re-exportsfrom utils.helpers import cleanUser must know exact module location✅ With __init__.py re-exportsfrom utils import cleanCleaner, user doesn't need internal details__init__.py acts as the public API for your package.It decides what's easy to import and what stays hidden inside.
This diagram shows how __init__.py re-exports names from internal modules, simplifying imports for users of the package. The bottom panels compare the import experience with and without re-exports.

Three Uses of __init__.py

Use CaseWhat Goes in __init__.pyBenefit
Empty fileNothing — just an empty fileMarks the directory as a package (minimum required)
Re-exportingfrom .helpers import cleanUsers can write from utils import clean without knowing internal structure
Defining __all____all__ = ["clean", "bold"]Controls what from utils import * exports
InitializationSetup code like logging config or default valuesRuns once when package is first imported

Common patterns for __init__.py usage

SECTION 6

Worked Example — Building a Game Utilities Package

Let's walk through creating a small package called game_tools that contains utilities for a simple game. We'll create two modules — one for scoring and one for player management — and connect them with __init__.py.

Creating the game_tools Package

  1. 1

    Step 1 — Create the Directory Structure

    First, create a folder called game_tools inside your project. Inside that folder, create three files: __init__.py, scoring.py, and player.py. Your structure should look like this:
    my_game/ → main.py + game_tools/(__init__.py, scoring.py, player.py)
  2. 2

    Step 2 — Write the scoring.py Module

    In scoring.py, define functions for calculating scores: def calculate_score(hits, misses): return hits * 10 - misses * 5 def get_grade(score): if score >= 90: return "A" elif score >= 70: return "B" else: return "C"
  3. 3

    Step 3 — Write the player.py Module

    In player.py, create a simple Player class: class Player: def __init__(self, name): self.name = name self.score = 0 def add_points(self, points): self.score += points return self.score
  4. 4

    Step 4 — Configure __init__.py

    In __init__.py, re-export the most important names so users can access them directly from the package: from .scoring import calculate_score, get_grade from .player import Player __all__ = ["calculate_score", "get_grade", "Player"]
    The dot in .scoring means "from the current package" — this is a relative import.
  5. 5

    Step 5 — Use the Package in main.py

    Now in main.py (which sits outside the package folder), you can import cleanly: from game_tools import Player, calculate_score p = Player("Alex") p.add_points(calculate_score(hits=8, misses=2)) print(f"{p.name} has {p.score} points")
    Output: Alex has 70 points — The package works! Users don't need to know whether Player lives in player.py or scoring.py.

SECTION 7

Modules vs. Packages — When to Use Each

Knowing when to use a standalone module versus a full package is an important design decision. A single module works great for small, focused functionality, while a package becomes essential when your code grows and needs internal organization.

FeatureModule (single .py file)Package (directory + __init__.py)
ComplexitySimple — one file, one topicStructured — multiple files, organized by subtopic
Best forSmall utility functions, constants, or a single classLibraries, larger features, related groups of modules
File count1 file1 directory + __init__.py + multiple .py files
NamespaceOne flat namespaceHierarchical (nested sub-packages possible)
Import syntaximport mymodulefrom mypackage import submodule
ScalabilityBecomes unwieldy past ~300 linesScales to thousands of lines across many files

Comparison of modules and packages

✦

KEY TAKEAWAY

Think of it like organizing your school notes. A single module is like one sheet of notes for a homework assignment — fine for small tasks. A package is like a binder with labeled dividers and multiple sheets — essential when you have an entire course to organize. The __init__.py file is the binder cover that identifies the collection as one unit.

SECTION 8

Connection to Advanced Concepts

The module and package system you've learned here is the foundation for more advanced Python concepts. As you progress, you'll encounter sub-packages, relative imports, virtual environments, and third-party package managers that all build on these ideas.

What You Learned TodayWhere It Leads
Creating modules (.py files)Writing reusable libraries that others can install via pip install
Creating packages with __init__.pyBuilding sub-packages (packages inside packages) for enterprise-level applications
Using the import statementUnderstanding relative imports (from . import module) and circular import prevention
Organizing project directoriesUsing setup.py or pyproject.toml to create distributable packages
Understanding namespacesWorking with namespace packages (PEP 420) and advanced import hooks

From basics to advanced Python packaging

💡

Looking Ahead

When you're ready for the next step, explore virtual environments (python -m venv myenv) which create isolated spaces for your project's packages. This prevents version conflicts between projects and is standard practice for professional Python development.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain the difference between a Python module and a Python package. Why is the __init__.py file important for packages?
Show answer
A module is a single .py file that contains Python code (functions, classes, variables). A package is a directory (folder) that contains one or more modules. The __init__.py file is crucial because it tells Python's import system that the directory should be treated as a package rather than a regular folder. Without it (in Python versions before 3.3), Python cannot import from the directory. Even in newer versions, including __init__.py is considered best practice because it makes the package's purpose explicit and provides a place to configure the package's public interface.

Question 2: BASIC CALCULATION

Given the following directory structure, write the correct import statement in main.py to use the add function from math_tools/arithmetic.py: project/ main.py math_tools/ __init__.py arithmetic.py # contains: def add(a, b): return a + b
Show answer
There are three valid approaches. First, you can write from math_tools.arithmetic import add and then call add(3, 5) directly. Second, you can write from math_tools import arithmetic and call arithmetic.add(3, 5). Third, if the __init__.py file contains from .arithmetic import add, you can simply write from math_tools import add. The first approach is the most common when you haven't set up re-exports.

Question 3: INTERMEDIATE

You have a package called shapes with two modules: circle.py (containing area() and circumference()) and rectangle.py (containing area() and perimeter()). Write the contents of __init__.py so that a user can write from shapes import circle_area, rect_area (using renamed imports to avoid the name conflict on area).
Show answer
In __init__.py, you would use the as keyword to rename the conflicting functions: from .circle import area as circle_area from .circle import circumference from .rectangle import area as rect_area from .rectangle import perimeter __all__ = ["circle_area", "circumference", "rect_area", "perimeter"] This solves the naming conflict by giving each area() function a unique name at the package level. The __all__ list ensures that from shapes import * only exports the intended names.

Question 4: APPLIED

You're building a weather app with the following structure. Design the package and explain what goes into each __init__.py: weather_app/ main.py weather/ __init__.py api.py # fetch_forecast(city) display.py # show_weather(data) data/ __init__.py cache.py # save_cache(), load_cache() history.py # get_history(city, days) The goal: a user of main.py should be able to write from weather import fetch_forecast, show_weather and from weather.data import load_cache, get_history.
Show answer
This involves two packages — weather (the outer package) and weather/data (a sub-package). In weather/__init__.py, write: from .api import fetch_forecast from .display import show_weather In weather/data/__init__.py, write: from .cache import load_cache, save_cache from .history import get_history The sub-package data/ needs its own __init__.py because it is a package within a package. Each __init__.py re-exports the names that form the public API of that level of the hierarchy.

Question 5: CRITICAL THINKING

A classmate argues: "Why bother with packages? I'll just put all my functions in one big file called everything.py." Provide at least three concrete reasons why using packages is better for a project with 500+ lines of code. Then explain one scenario where a single module would actually be the better choice.
Show answer
Three reasons packages are better for large projects: (1) Readability — finding a function in a 50-line module named scoring.py is far easier than scrolling through 500+ lines in one file. (2) Collaboration — when multiple people work on the same project, splitting code across files reduces merge conflicts because teammates edit different files instead of all changing the same one. (3) Reusability — a well-organized module can be imported into other projects without bringing along unrelated code. However, a single module is the better choice when you have a small, focused utility — for example, a config.py file that just stores constants and settings. Creating a whole package for 20 lines of configuration would be over-engineering the solution.

SUMMARY

Lesson Summary

Summary

In this lesson, you learned that a module is any single .py file containing reusable Python code, while a package is a directory of related modules tied together by the special __init__.py file. The import statement searches Python's module cache and sys.path directories to find and load your modules, and each module creates its own namespace to prevent naming conflicts.

You practiced configuring __init__.py to re-export names for a cleaner public API and used __all__ to control wildcard imports. Remember: use a single module for small, focused code and upgrade to a package when your project grows beyond a few hundred lines. These organizational skills are the foundation for building professional Python projects, creating installable libraries, and collaborating with other developers.

Varsity Tutors • Python • Organizing Modules & Packages