One file called __init__.py is all that separates a random folder of scripts from a real, importable library. Python packages are how projects scale past a handful of modules: group related files in a directory, add that one file, and the whole folder imports as a unit. This guide covers creating packages, relative imports, __all__, and layouts that stay clean as a project grows.
“The structure of a software system reflects the social structure of the organization that built it.”
Melvin Conway
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 11 minutes
A single utils.py file works fine until it hits 500 lines. Then you split it into string_utils.py, file_utils.py, and date_utils.py. Now you have six loose files sitting in one folder with no real structure. A package is Python’s answer. It is a folder with an __init__.py file inside, and that folder behaves like one importable unit. You import from it the same way you import a module, but inside it can hold dozens of modules, sub-folders, and a clean hierarchy.
Think of a package like a kitchen drawer organizer. Loose modules are like spoons, forks, and knives all dumped in one drawer. A package gives each group its own labelled tray, so you always know where to reach. Every serious Python project, such as Flask, NumPy, and Django, is organized as packages for exactly this reason. This post shows you how to create them, how __init__.py controls what gets exported, and how relative imports work inside packages.
Table of Contents
Your First Package
A package is just a folder with an __init__.py file inside it. That one file is the sign on the door that tells Python “this folder is importable.” Here is the shape of a small package and the pieces that make it work.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows how Python packages are built from directories. A folder with an __init__.py file becomes a package, and subfolders with their own __init__.py become sub-packages. The import styles shown, an absolute path like from mypackage.utils import clean, a relative one like from .utils import clean, or a plain import mypackage, are just different ways of walking this tree. Once this folder-to-import mapping clicks, you can organize any project bigger than a single file.
📄 Project structure
my_project/
├── main.py
└── analytics/
├── __init__.py
├── stats.py
└── charts.py
📄 analytics/stats.py: a module inside the package
"""Statistical functions for our analytics package."""
def mean(numbers):
return sum(numbers) / len(numbers)
def median(numbers):
sorted_nums = sorted(numbers)
n = len(sorted_nums)
mid = n // 2
if n % 2 == 0:
return (sorted_nums[mid - 1] + sorted_nums[mid]) / 2
return sorted_nums[mid]
📄 analytics/charts.py: another module in the package
"""Simple text-based chart functions."""
def bar_chart(data, label="Value"):
print(f"\n{label}:")
for name, value in data.items():
bar = "█" * int(value / 5)
print(f" {name:<12} {bar} {value}")
📄 analytics/__init__.py: the package initializer
"""Analytics package: stats and visualization tools.""" from .stats import mean, median from .charts import bar_chart __all__ = ["mean", "median", "bar_chart"]
Now main.py can use the package. Say five teammates, Rahul, Aditi, Viraj, Anvi, and Vinay, just finished a project sprint, and you want a quick summary of their review scores.
📄 main.py: using the package
# Import directly from the package (thanks to __init__.py)
from analytics import mean, median, bar_chart
scores = {"Rahul": 92, "Aditi": 88, "Viraj": 95, "Anvi": 84, "Vinay": 91}
values = list(scores.values())
print(f"Mean score: {mean(values):.1f}")
print(f"Median score: {median(values)}")
bar_chart(scores, label="Team Scores")
▶ Output
Mean score: 90.0 Median score: 91 Team Scores: Rahul ██████████████████ 92 Aditi █████████████████ 88 Viraj ███████████████████ 95 Anvi ████████████████ 84 Vinay ██████████████████ 91
What happened here: The analytics/ folder is a package because it contains __init__.py. That file uses relative imports (from .stats import mean) to lift the useful names up to the top of the package. So a user can write the short from analytics import mean instead of the longer from analytics.stats import mean. The __all__ list is the package’s public menu: it decides what gets handed over when someone writes from analytics import *.
Relative vs Absolute Imports
📄 Inside a package, you have two import styles
# File: analytics/advanced.py # ABSOLUTE import: full path from the project root from analytics.stats import mean # RELATIVE import: relative to the current package from .stats import mean # same folder (one dot) from .charts import bar_chart # same folder (one dot) from ..utils import clean_data # parent package (two dots)
Relative imports use dots, a bit like saying “the room next door” instead of giving the full street address. One dot (.) means “same package,” two dots (..) means “parent package.” They only work inside packages, so you cannot use them in a script you run directly. A good habit: use relative imports inside a package (they keep working even if you rename the top-level package) and absolute imports everywhere else.
Sub-Packages: Packages Inside Packages
📄 Nested package structure
ecommerce/
├── __init__.py
├── cart/
│ ├── __init__.py
│ ├── models.py
│ └── pricing.py
├── users/
│ ├── __init__.py
│ ├── auth.py
│ └── profiles.py
└── payments/
├── __init__.py
├── stripe_handler.py
└── razorpay_handler.py
📄 ecommerce/cart/pricing.py
"""Pricing calculations for the cart."""
def apply_discount(price, discount_pct):
"""Apply percentage discount to a price."""
return price * (1 - discount_pct / 100)
def calculate_tax(price, tax_rate=18):
"""Calculate GST (India's sales tax) on price."""
return price * tax_rate / 100
def final_price(price, discount_pct=0, tax_rate=18):
"""Calculate final price after discount and tax."""
discounted = apply_discount(price, discount_pct)
tax = calculate_tax(discounted, tax_rate)
return discounted + tax
📄 Using sub-packages
from ecommerce.cart.pricing import final_price, apply_discount
laptop_price = 75000
discounted = apply_discount(laptop_price, 10)
total = final_price(laptop_price, discount_pct=10, tax_rate=18)
print(f"Original: ₹{laptop_price:,.0f}")
print(f"After 10%: ₹{discounted:,.0f}")
print(f"After GST: ₹{total:,.0f}")
▶ Output
Original: ₹75,000 After 10%: ₹67,500 After GST: ₹79,650
What happened here: Sub-packages work exactly like packages. Each folder needs its own __init__.py. You can nest as deep as you like, but more than three levels usually means it is time to rethink the layout. The dot-separated import path is just a map of the folders: ecommerce.cart.pricing walks straight to ecommerce/cart/pricing.py, like a postal address reading country, city, street.
What Goes in __init__.py?
📄 __init__.py patterns from beginner to advanced
# Pattern 1: Empty (just marks directory as a package)
# analytics/__init__.py
# (an empty file is perfectly valid)
# Pattern 2: Re-export public API (most common)
# analytics/__init__.py
from .stats import mean, median, std_dev
from .charts import bar_chart, pie_chart
__all__ = ["mean", "median", "std_dev", "bar_chart", "pie_chart"]
# Pattern 3: Package-level setup
# analytics/__init__.py
import logging
logger = logging.getLogger(__name__)
logger.info("Analytics package loaded")
from .stats import mean, median
__version__ = "1.2.0"
# Pattern 4: Lazy loading (advanced, for large packages)
# analytics/__init__.py
def __getattr__(name):
if name == "heavy_model":
from .ml import heavy_model
return heavy_model
raise AttributeError(f"module 'analytics' has no attribute '{name}'")
Pattern 2 is the one you will reach for almost every time. It gives you a clean public Application Programming Interface (API): people import straight from the package name, and you decide which internal names are on show. Pattern 4 uses Python’s module-level __getattr__ hook, and big libraries like NumPy lean on it to skip loading heavy submodules until you actually touch them. Think of it like a restaurant that only fires up the pizza oven when someone finally orders pizza.
The Catch: Running Modules Inside Packages
❌ This fails: running a package module directly
$ python analytics/advanced.py
Traceback (most recent call last):
File "analytics/advanced.py", line 2, in <module>
from .stats import mean
ImportError: attempted relative import with no known parent package
✅ Fix: run it as a module with the -m flag
$ python -m analytics.advanced # Works. Python now knows the parent package, so the relative import resolves.
Think of a train coach: coach S4 makes sense as “the one after S3” only while it is attached to the train. Detach it, and “the previous coach” points at nothing. When you run python analytics/advanced.py, Python treats that file as a lone script with no package around it, a detached coach. The relative import then fails because there is no “parent” for the dots to point back to. Running python -m analytics.advanced instead tells Python to start the file as part of the analytics package, so the parent context is there and the dots resolve. Same file, two different ways to launch it, and only one of them keeps the package context alive.
Namespace Packages (No __init__.py)
Since Python 3.3, a folder without an __init__.py can still act as a “namespace package.” This lets one logical package spread its content across several folders, like one company team sitting across two office buildings but still answering to the same team name. A few large plugin-style projects rely on this. In everyday code you almost never need it. Just drop an __init__.py into every package folder. It is explicit, it is clear, and it will not surprise the next person reading your code.
Common Mistakes
❌ Mistake 1: Forgetting __init__.py
# Without __init__.py, explicit imports still work in Python 3.3+ # BUT: __all__ stops mattering, IDE help gets worse, and intent is unclear # Always create __init__.py, even if it is empty
❌ Mistake 2: Circular imports between sub-modules
# analytics/stats.py imports from analytics/charts.py # analytics/charts.py imports from analytics/stats.py # Same circular import problem as with regular modules! # Fix: restructure, or use function-level imports
❌ Mistake 3: Too-deep nesting
# Bad: from myapp.core.models.user.serializers.v2 import UserSchema # If your import path is longer than ~3 dots, your structure is too deep # Use __init__.py to flatten the public API
Conclusion
Python packages group related modules into folders. The __init__.py file turns a folder into a package, relative imports keep references inside the package short and rename-proof, and sub-packages let you model bigger project structures. One rule saves the most pain: always run packages from the project root, never from inside the package folder.
You can now build your own packages. In the standard library tutorial, you will meet the 20 most useful modules that already ship with Python, the batteries-included tools that save you from reinventing the wheel. And if you want the full roadmap from basics to AI/ML projects, browse the Python + AI/ML tutorial series home. Structure your Python packages this way from day one and imports stay boring, which is exactly what you want.
Frequently Asked Questions
What is __init__.py and is it required?
__init__.py marks a directory as a Python package. It runs when the package is first imported. Since Python 3.3, directories work as ‘namespace packages’ without it, but you should always include __init__.py for explicit package declaration, __all__ support, and better IDE compatibility.
What is the difference between a module and a package?
A module is a single .py file (e.g., utils.py). A package is a directory containing __init__.py and one or more modules (e.g., analytics/ containing stats.py, charts.py). Packages organize related modules into a hierarchy. Both are importable.
What are relative imports in Python?
Relative imports use dots to reference the current package: from .stats import mean (same directory) or from ..utils import clean (parent package). They only work inside packages, not in scripts run directly. Use python -m package.module to preserve package context.
What does __all__ do in __init__.py?
__all__ is a list of names that should be exported when someone writes from package import *. It defines your package’s public API. Names not in __all__ are still accessible via explicit import, but won’t be included in wildcard imports.
How do I run a module inside a package?
Do not run python package/module.py, because relative imports will fail. Instead use python -m package.module, which keeps the package context. That tells Python to treat the file as part of a package, not as a standalone script.
Try It Yourself
Build a shapes/ package with three modules: circle.py, rectangle.py, and triangle.py. Give each module an area() and a perimeter() function. In __init__.py, re-export all six functions so users can import them straight from shapes. Then write a main.py that imports from the package and prints the area of each shape. As a bonus, try running one module directly with python shapes/circle.py and watch the relative import break, then fix it with python -m shapes.circle.
Interview Questions on Python Packages
How interviewers actually probe this topic: real scenarios, with answers you can say out loud.
Q: You add a new reports.py module to your analytics package, but from analytics import generate_report fails with ImportError while from analytics.reports import generate_report works fine. What is going on?
The package’s __init__.py never re-exports the new name. from analytics import X only finds names that __init__.py defines or imports; it does not scan submodule files for you. Add from .reports import generate_report to analytics/__init__.py, and add the name to __all__ if the package maintains one. After that, both import styles work.
Q: Two modules inside your package import names from each other, and you start seeing “cannot import name X from partially initialized module”. How do you fix it?
That is a circular import: each module needs the other before either finishes loading. The cleanest fix is to move the shared code into a third module that both can import. If restructuring is too costly right now, move one of the imports inside the function that actually uses it, so it runs at call time instead of load time. A third option is to import the module itself (import analytics.charts) and access attributes lazily rather than pulling names at the top of the file.
Q: Your team renames the top-level package from analytics to insights, and dozens of imports across the codebase break. Which import style would have avoided this inside the package?
Relative imports. from .stats import mean never mentions the package name, so renaming the top-level folder costs nothing inside it. Absolute imports like from analytics.stats import mean hardcode the old name in every file that uses them. The working rule: relative imports inside a package, absolute imports everywhere else.
Q: What exactly runs when you write import ecommerce.cart.pricing?
Python walks the dotted path top-down: ecommerce/__init__.py executes first, then ecommerce/cart/__init__.py, then pricing.py runs top to bottom. Each finished module is cached in sys.modules, so importing the same path again anywhere in the program is a cheap dictionary lookup, not a re-execution. This is why side effects in __init__.py fire only once per process.
Q: Your package imports slowly because __init__.py eagerly pulls in heavy submodules most users never touch. How do you fix that without breaking the public API?
Use lazy loading with a module-level __getattr__ in __init__.py (PEP 562, available since Python 3.7). Remove the heavy name from the eager imports and import it inside __getattr__ only when someone first accesses it; the attribute still looks like it lives on the package, so callers change nothing. Large libraries such as NumPy and SciPy use exactly this trick. Function-level imports are a simpler fallback for one-off cases.
Q: When would you actually choose a namespace package over a regular one?
Almost never in ordinary application code. Namespace packages earn their keep when one logical package must be assembled from multiple directories or separately installed distributions, the classic case being plugin ecosystems where independent packages all contribute modules under a shared top-level name. For everything else, a regular package with an explicit __init__.py is clearer, supports __all__, and behaves more predictably with tooling.
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: Python: Modules, Creating, Importing, Organizing Code
Next: Python: Standard Library, 20 Must-Know Modules
Series Home: Python + AI/ML Tutorial Series

No comment