Organizing Code into Packages and Subpackages
Course: Python Training
Topic: Organizing Code into Packages and Subpackages
Course Link: Python Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Packages and Subpackages
As Python applications grow, keeping every function, class, and variable in a single file becomes difficult to manage. A large file can be challenging to understand, test, debug, and maintain. Python supports a modular structure that allows developers to divide applications into separate modules, packages, and subpackages.
A module is generally a Python file containing reusable code. A package organizes related modules under a common namespace, while a subpackage is a package nested inside another package. This hierarchy makes it easier to group code by feature, responsibility, or application layer.
For example, an e-commerce application might have separate packages for products, customers, orders, payments, and reports. Each package can contain modules responsible for specific operations.
Learning Objectives
- Understand the difference between modules, packages, and subpackages.
- Design a logical directory structure for a Python project.
- Create regular packages using
__init__.py.
- Import modules and functions from packages and subpackages.
- Understand absolute imports and relative imports.
- Use package initialization files appropriately.
- Recognize circular imports and common package-related errors.
- Apply package organization principles to a practical project.
2. Understanding Modules, Packages, and Subpackages
2.1 Module
A module is usually a single Python file ending with .py. It can contain functions, classes, constants, and other statements.
Example: calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
Another file can import this module and reuse its functions instead of duplicating the code.
import calculator
print(calculator.add(10, 5))
print(calculator.subtract(10, 5))
Output:
15
5
2.2 Package
A package is a way to group related modules under a shared namespace. A traditional regular package commonly contains an __init__.py file.
For example, a package named calculations might contain separate modules for arithmetic, percentages, and statistics.
calculations/
__init__.py
arithmetic.py
percentage.py
statistics_tools.py
2.3 Subpackage
A subpackage is a package located inside another package. It allows a project to create additional levels of organization.
myapp/
__init__.py
users/
__init__.py
models.py
services.py
products/
__init__.py
models.py
services.py
In this example, users and products are subpackages of myapp. Their modules can be imported using dotted paths, such as myapp.users.models.
Comparison Table
| Concept |
Meaning |
Example |
| Module |
A Python file containing reusable code |
models.py |
| Package |
A namespace that groups related modules |
myapp |
| Subpackage |
A package nested within another package |
myapp.users |
| Import path |
The dotted name used to locate a module or package |
myapp.users.models |
3. Why Organize Code into Packages?
Packages provide a structured way to develop and maintain applications as their size increases.
- Better organization: Related modules are kept together.
- Code reuse: Common functionality can be imported by multiple parts of an application.
- Maintainability: Changes can be made within a specific area without searching through one large file.
- Readability: Package names communicate the purpose of different parts of the application.
- Team collaboration: Developers can work on separate features with clearer boundaries.
- Testing: Individual modules and features can be tested independently.
- Scalability: New functionality can be added without putting all code in one place.
- Namespace separation: Dotted import paths help distinguish modules with similar names.
4. Understanding the __init__.py File
The __init__.py file is commonly used to identify and initialize a regular Python package. It can be empty or contain package-level code, imports, constants, and public API definitions.
Python also supports namespace packages, which can exist without an __init__.py file. However, using __init__.py is a clear and familiar approach for many application projects.
Example Directory Structure
shop/
__init__.py
products.py
customers.py
Example: products.py
def get_product_name():
return "Laptop"
Example: __init__.py
"""Shop package."""
PACKAGE_NAME = "Shop Management"
Importing the Package
import shop
print(shop.PACKAGE_NAME)
Output:
Shop Management
Importing a regular package executes its __init__.py file when that package is initialized. It does not automatically import every module inside the package.
5. Creating a Python Package Step by Step
Consider a package named math_tools that provides basic arithmetic operations.
Step 1: Create the Project Structure
python_project/
main.py
math_tools/
__init__.py
addition.py
subtraction.py
Step 2: Add Code to addition.py
def add(a, b):
return a + b
Step 3: Add Code to subtraction.py
def subtract(a, b):
return a - b
Step 4: Import Functions in main.py
from math_tools.addition import add
from math_tools.subtraction import subtract
print("Addition:", add(20, 10))
print("Subtraction:", subtract(20, 10))
Output:
Addition: 30
Subtraction: 10
This structure separates each operation into its own module and allows the main program to use the functions through explicit imports.
6. Organizing Code into Subpackages
When a package contains many modules, it can be divided into subpackages. Each subpackage groups a more specific area of functionality.
Example: A Learning Management Application
learning_app/
__init__.py
students/
__init__.py
models.py
services.py
courses/
__init__.py
models.py
services.py
payments/
__init__.py
processor.py
reports/
__init__.py
student_reports.py
main.py
The structure separates students, courses, payments, and reports. Each area can contain multiple modules as the application grows.
| Package or Subpackage |
Responsibility |
learning_app.students |
Student-related models and operations |
learning_app.courses |
Course information and management |
learning_app.payments |
Payment-related functionality |
learning_app.reports |
Report generation and summaries |
Example: students/models.py
class Student:
def __init__(self, name, email):
self.name = name
self.email = email
def display(self):
return f"{self.name} ({self.email})"
Example: students/services.py
def register_student(name, email):
return {
"name": name,
"email": email,
"registered": True
}
Example: main.py
from learning_app.students.models import Student
from learning_app.students.services import register_student
student = Student("Meera", "[email protected]")
registration = register_student(student.name, student.email)
print(student.display())
print(registration["registered"])
Output:
Meera ([email protected])
True
Run the program from the project root using an appropriate command such as python main.py, assuming the files and package structure match the example.
7. Absolute Imports
An absolute import specifies a module using its full path from the importable top-level package.
Example
from learning_app.students.models import Student
from learning_app.courses.models import Course
Absolute imports are often easy to understand because they clearly identify the source package. They work well in medium-sized and large projects where clarity is important.
Advantages of Absolute Imports
- Clearly show where imported functionality is located.
- Are easier to understand when reading a module independently.
- Reduce ambiguity when packages have multiple levels.
- Help developers navigate larger codebases.
8. Relative Imports
Relative imports refer to modules based on the location of the current package. They use leading dots to represent the current package or parent packages.
Consider this structure:
learning_app/
__init__.py
students/
__init__.py
models.py
services.py
validators.py
Example: Import a Module from the Same Package
Inside students/services.py:
from .validators import validate_email
A single leading dot refers to the current package, so this import looks for validators in the same students package.
Example: Import from a Parent Package
Inside a subpackage module, two leading dots refer to the parent package level.
from ..config import DATABASE_NAME
This example assumes that a module named config.py exists in the parent package and defines DATABASE_NAME.
Relative Import Notation
| Syntax |
Meaning |
from . import module |
Import a module from the current package. |
from .module import function |
Import a name from a module in the current package. |
from .. import module |
Refer to a module in the parent package. |
from ..subpackage import module |
Refer to a subpackage under the parent package. |
Important: Relative imports rely on package context. They can fail when a module is executed directly as a standalone file instead of being imported or run within its package context. For example, use python -m learning_app.students.services from the appropriate project root when module execution is suitable.
9. Using __init__.py to Define a Package API
A package can expose selected functions and classes through its __init__.py file. This creates a convenient public interface for users of the package.
Example Structure
geometry/
__init__.py
circle.py
rectangle.py
circle.py
import math
def area(radius):
return math.pi * radius ** 2
rectangle.py
def area(length, width):
return length * width
__init__.py
from .circle import area as circle_area
from .rectangle import area as rectangle_area
__all__ = ["circle_area", "rectangle_area"]
Using the Package API
from geometry import circle_area, rectangle_area
print(round(circle_area(3), 2))
print(rectangle_area(4, 5))
Output:
28.27
20
Aliasing the two functions avoids a naming conflict because both original modules define a function named area. The __all__ variable documents the package's intended public names for wildcard imports. It does not prevent other module attributes from being accessed directly.
10. Understanding Namespaces in Packages
A namespace is a mapping between names and the objects they represent. Packages create a hierarchy of namespaces that helps separate functionality and avoid collisions.
For example, two subpackages may each define a class named Model:
from learning_app.students.models import Student
from learning_app.courses.models import Course
Even if the student and course modules each contain a class named Model, their fully qualified module paths distinguish them. Choosing descriptive import aliases can further improve clarity.
Example of Aliasing Imported Names
from learning_app.students import models as student_models
from learning_app.courses import models as course_models
This style makes the source of each module explicit when both contain similarly named classes or functions.
11. The Difference Between a Package and a Namespace Package
Python supports regular packages and namespace packages. Knowing the difference is useful when building libraries or working with complex project structures.
| Feature |
Regular Package |
Namespace Package |
__init__.py |
Commonly contains this file. |
Can exist without this file. |
| Initialization code |
Can run code from __init__.py. |
Does not use a package-level __init__.py in the same way. |
| Organization |
Suitable for most application packages. |
Can combine portions of a namespace across multiple locations. |
| Typical use |
Application features and standard libraries. |
Specialized library ecosystems that share a namespace. |
For most learning projects and application code, regular packages with clear __init__.py files are a straightforward choice. Namespace packages are useful for more specialized distribution and plugin scenarios.
12. Running Python Code Inside a Package
How a module is launched can affect how imports are resolved. A module run directly by its file path may not have the package context required for relative imports.
Example Structure
project/
app/
__init__.py
__main__.py
operations.py
operations.py
def greet():
return "Welcome to the application!"
__main__.py
from .operations import greet
def main():
print(greet())
if __name__ == "__main__":
main()
From the project directory, run:
python -m app
Output:
Welcome to the application!
Python executes the package's __main__.py file when the package is launched with python -m app. This is a useful pattern for packages that provide a command-line entry point.
13. Avoiding Circular Imports
A circular import occurs when two or more modules depend on each other in a cycle. For example, users.py imports orders.py, while orders.py also imports users.py.
Example of a Potential Circular Dependency
# users.py
from orders import get_orders
def get_user():
return {"name": "Meera", "orders": get_orders()}
# orders.py
from users import get_user
def get_orders():
return ["Order-101"]
This structure can cause import failures or partially initialized module errors, depending on which names are accessed and when the imports occur.
Ways to Reduce Circular Imports
- Move shared functionality into a separate module imported by both modules.
- Reconsider whether the modules have clearly separated responsibilities.
- Use function-local imports only when there is a justified need to defer an import.
- Pass required objects as function arguments rather than importing an entire module unnecessarily.
- Keep dependencies flowing in a clear direction, such as models → services → application entry point.
Good package design aims to minimize unnecessary dependencies between sibling modules.
14. Common Package-Related Errors
| Error or Problem |
Possible Cause |
Solution |
ModuleNotFoundError |
Python cannot find the package or module. |
Check the directory structure, import path, environment, and spelling. |
ImportError |
The requested name cannot be imported. |
Verify the name exists and is exposed by the module or package. |
| Relative import failure |
The file is run without package context. |
Run it as a module using python -m from the project root when appropriate. |
| Circular import |
Modules depend on one another in a cycle. |
Separate shared functionality and simplify dependencies. |
| Wrong module imported |
A local file shadows a package or standard library module. |
Rename conflicting files and check the import path. |
| Package member unavailable |
The module was assumed to be imported automatically. |
Import the module explicitly or expose the desired name through __init__.py. |
Example: Debugging an Import
import sys
print("Python executable:", sys.executable)
print("Import search path:")
for path in sys.path:
print(path)
This code helps inspect the Python interpreter and locations used for module discovery. If an import fails, verify that the correct virtual environment or interpreter is active.
15. Recommended Package Design Practices
- Use meaningful names: Prefer names such as
payments, reports, and authentication over vague names.
- Group by responsibility: Keep related functions and classes together.
- Keep modules focused: A module should have a clear, understandable purpose.
- Keep imports explicit: Prefer readable imports that reveal the source of a dependency.
- Limit package initialization: Avoid performing expensive operations or starting services just because a package is imported.
- Define public interfaces: Use documented exports where it improves usability.
- Reduce circular dependencies: Design modules with clear dependency directions.
- Use consistent naming: Module and package names are typically lowercase, with underscores when needed.
- Write tests: Test modules independently and test important interactions between packages.
- Document structure: Explain where important features and entry points are located.
- Use a project root: Run commands from a documented location so imports behave predictably.
- Separate application code from tests: Use a clear test directory and a consistent test naming convention.
16. Practical Mini-Project: Employee Management Package
Objective: Build a small employee management application with packages and subpackages. The project separates employee records, salary calculations, and report generation.
Step 1: Create the Directory Structure
employee_app/
__init__.py
main.py
employees/
__init__.py
records.py
payroll/
__init__.py
salary.py
reports/
__init__.py
summary.py
Step 2: Create employees/records.py
def create_employee(name, employee_id, base_salary):
return {
"name": name,
"employee_id": employee_id,
"base_salary": base_salary
}
Step 3: Create payroll/salary.py
def calculate_annual_salary(monthly_salary):
return monthly_salary * 12
Step 4: Create reports/summary.py
def employee_summary(employee, annual_salary):
return (
f"Employee: {employee['name']}\n"
f"ID: {employee['employee_id']}\n"
f"Annual salary: {annual_salary}"
)
Step 5: Create main.py
from employee_app.employees.records import create_employee
from employee_app.payroll.salary import calculate_annual_salary
from employee_app.reports.summary import employee_summary
employee = create_employee("Aarav", "EMP101", 50000)
annual_salary = calculate_annual_salary(employee["base_salary"])
print(employee_summary(employee, annual_salary))
Expected Output
Employee: Aarav
ID: EMP101
Annual salary: 600000
How to Run the Project
Place the employee_app directory inside a project root, and create a runner file at that root if needed:
# run_app.py
from employee_app.main import *
A cleaner option is to make employee_app executable as a package by adding a __main__.py file and then running python -m employee_app from the project root. For this approach, place the application entry-point logic in employee_app/__main__.py and keep imports consistent with the package name.
Project improvement ideas:
- Add employee validation and reject invalid salary values.
- Create a module to store and retrieve multiple employee records.
- Add CSV or JSON export functionality.
- Write unit tests for salary calculations and report formatting.
- Document the public functions in each module.
17. Practice Exercises
- Create a package named
conversions with modules for temperature and distance conversions.
- Create a subpackage named
conversions.temperature that converts Celsius to Fahrenheit and Fahrenheit to Celsius.
- Import a function using both an absolute import and a relative import.
- Create a package-level constant in
__init__.py and access it from another file.
- Build a
school package with students, teachers, and courses subpackages.
- Identify a circular import in a sample project and refactor the shared functionality into a third module.
- Create a package with a
__main__.py file and run it using python -m.
- Investigate a
ModuleNotFoundError by checking the interpreter and sys.path.
- Write unit tests for functions in two different subpackages.
- Design a package structure for a small online shopping application and explain each package's responsibility.
18. Interview Questions and Answers
Q1. What is a Python package?
A package is an importable namespace that organizes related Python modules. A regular package commonly contains an __init__.py file.
Q2. What is a subpackage?
A subpackage is a package nested within another package. It helps organize a large codebase into smaller feature-based areas.
Q3. What is the purpose of __init__.py?
It can identify and initialize a regular package, define package-level attributes, and expose selected names. It may be empty if no initialization logic is required.
Q4. Does every Python package require __init__.py?
No. Python supports namespace packages that can exist without __init__.py. Regular packages commonly use this file, particularly in application projects.
Q5. What is the difference between absolute and relative imports?
Absolute imports specify a module path from the top-level importable package. Relative imports use leading dots to refer to the current or parent package.
Q6. What does from .module import function mean?
It imports function from a module named module in the current package.
Q7. What is a circular import?
A circular import occurs when modules depend on one another in a cycle. It can result in partially initialized modules or import errors.
Q8. Why should package initialization code be kept simple?
Package initialization runs when a regular package is initialized. Expensive operations or unexpected side effects can slow imports and make applications harder to test.
Q9. How can a package provide a convenient public API?
It can import selected functions and classes into its __init__.py file, allowing callers to import them directly from the package.
Q10. How do you run a Python package as a program?
A package can include a __main__.py file. Running python -m package_name executes that file when the package is importable from the current environment.
Q11. Why might Python import the wrong module?
A local file or directory can shadow another module with the same name. Inspect filenames, the active interpreter, and sys.path to diagnose the issue.
Q12. What are the benefits of organizing code into subpackages?
Subpackages improve separation of concerns, readability, code reuse, testing, collaboration, and long-term maintainability.
19. Quick Revision Checklist
- Understand modules, packages, and subpackages.
- Know the purpose of
__init__.py.
- Build a package using separate Python files.
- Organize related features into subpackages.
- Use absolute imports and relative imports appropriately.
- Understand package namespaces and public APIs.
- Recognize circular imports and resolve them through better design.
- Know how
__main__.py and python -m work together.
- Debug import errors by checking paths and the Python environment.
- Practice building and testing a multi-package application.
20. Conclusion
Organizing Python code into packages and subpackages is an essential skill for building maintainable applications. Modules separate individual units of functionality, packages group related modules, and subpackages add structure as a project grows. By using meaningful names, clear import paths, simple initialization files, and well-defined responsibilities, developers can create Python projects that are easier to understand, test, extend, and maintain.
Additional Learning Resources