Beginner/Intermediate
Every Python developer reaches a moment when a single file stops working. You’re 500 lines in, juggling functions from data processing, API calls, and database operations all in one main.py, and suddenly finding the function you need feels like a scavenger hunt. This is when organizing your code into multiple files transforms from a nice-to-have into a survival skill.
The good news? Python has a built-in system for this. You don’t need external tools or elaborate frameworks—the module and package system is already there, waiting for you to use it. Whether you’re building a command-line tool, a web application, or a data science project, splitting your code into logical, reusable pieces makes everything cleaner, faster to debug, and easier for others to understand.
In this article, we’ll explore how Python modules and packages work, walk through real examples from organizing a few scripts to building a complete project structure, and learn the best practices that professionals use every day. By the end, you’ll understand __init__.py files, import patterns, and how to structure projects that scale.
Splitting Python Code Into Multiple Files: Quick Example
Let’s start with the simplest possible example. Imagine you have a script that needs utility functions. Instead of writing everything in one file, split it into two:
# utils.py
def greet(name):
return f"Hello, {name}!"
def add_numbers(a, b):
return a + b
Now import and use those functions from a separate file:
# main.py
from utils import greet, add_numbers
print(greet("Alice"))
print(f"3 + 4 = {add_numbers(3, 4)}")
Output (when main.py runs):
Hello, Alice!
3 + 4 = 7
That’s it. One file defines functions, another imports and uses them. This simple pattern scales to complex projects. The rest of this article teaches you how to expand this concept into packages, subdirectories, and professional-grade structures.
What Are Modules and Packages?
Python uses two core concepts to organize code: modules and packages. Understanding the difference is crucial because they work together but serve different purposes.
A module is simply a Python file. When you create utils.py, you’ve created a module named utils. Inside it, you can define functions, classes, and variables. Other files import and use what you define. A module is the smallest unit of code organization.
A package is a directory that contains Python modules and a special __init__.py file. Packages let you organize related modules into a hierarchical structure. Think of a module as a single document and a package as a folder containing multiple documents (modules).
Here’s a quick comparison:
| Concept | What It Is | Example | How to Import |
|---|---|---|---|
| Module | A single Python file | utils.py |
import utils or from utils import func |
| Package | A directory with __init__.py and modules |
mypackage/ directory |
import mypackage.module or from mypackage import module |
| Namespace Package | A directory without __init__.py (Python 3.3+) |
mypackage/ (no __init__.py) |
import mypackage.module (if properly configured) |
Think of it this way: a module is like a notebook, and a package is like a filing cabinet full of notebooks. When you want something from one notebook, you ask for it by name. When you want something from a notebook in the cabinet, you specify both the cabinet and the notebook.
Importing From Files in the Same Directory
The simplest form of code splitting happens when all your files live in the same directory. Let’s build on the earlier example and explore different import styles.
Start with a file structure like this:
# Project structure:
# project/
# ├── main.py
# └── utils.py
Now, let’s write a more realistic utility module:
# utils.py
"""Utility functions for text processing."""
def reverse_string(text):
"""Reverse a string."""
return text[::-1]
def count_vowels(text):
"""Count vowels in a string."""
vowels = "aeiouAEIOU"
return sum(1 for char in text if char in vowels)
def format_title(text):
"""Format text as a title."""
return text.title()
In your main script, you can import from utils several ways. Let’s start with importing the entire module:
# main.py - Import Approach 1: Import entire module
import utils
text = "hello world"
print(f"Original: {text}")
print(f"Reversed: {utils.reverse_string(text)}")
print(f"Vowels: {utils.count_vowels(text)}")
Output (Approach 1):
Original: hello world
Reversed: dlrow olleh
Vowels: 3
Or use selective imports:
# main.py - Import Approach 2: Import specific functions
from utils import reverse_string, count_vowels
text = "python programming"
print(f"Reversed: {reverse_string(text)}")
print(f"Vowels: {count_vowels(text)}")
Output (Approach 2):
Reversed: gnimmargorp nohtyp
Vowels: 7
When you import a module from the same directory, Python searches the current directory automatically. If your files are in different directories (like one folder for the main app and another for utilities), you’ll use packages instead.
Creating Packages With Directories
Real projects need structure. Instead of dumping all modules in one directory, you organize related modules into packages. A package is a directory with an __init__.py file inside it.
Here’s a typical project structure:
# Project structure:
# weather_app/
# ├── main.py
# ├── data/
# │ ├── __init__.py
# │ └── weather_data.py
# └── utils/
# ├── __init__.py
# └── formatters.py
The data and utils directories are packages because they contain __init__.py files. Now you can import from these packages:
# weather_data.py (inside data/ package)
def fetch_temperature(location):
"""Simulated weather data fetch."""
return {"location": location, "temp_c": 22, "condition": "Sunny"}
Next, create the display formatter in the utils package. This module handles converting and presenting the raw data:
# formatters.py (inside utils/ package)
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def format_weather(data):
"""Format weather data for display."""
fahrenheit = celsius_to_fahrenheit(data["temp_c"])
return f"{data['location']}: {data['temp_c']}°C ({fahrenheit:.1f}°F), {data['condition']}"
Finally, the main script imports from both packages and ties everything together:
# main.py (at project root)
from data.weather_data import fetch_temperature
from utils.formatters import format_weather
location = "London"
weather = fetch_temperature(location)
print(format_weather(weather))
Output (when main.py runs):
London: 22°C (71.6°F), Sunny
When Python sees from data.weather_data import fetch_temperature, it looks for a directory named data with an __init__.py file, then finds the weather_data module inside it. Without the __init__.py file, Python won’t recognize data as a package, and the import will fail.
The __init__.py File Explained
The __init__.py file is how Python knows a directory is a package. Even if the file is empty, its presence tells Python “this directory should be treated as a package.” But __init__.py can do much more than just mark a directory.
An empty __init__.py file does nothing visible, but it still serves a purpose:
# utils/__init__.py (empty)
# This file exists, but is completely empty.
# Python still recognizes utils/ as a package.
However, you can use __init__.py to control what gets imported when someone imports your package. This is called the package’s public interface:
# math_tools/__init__.py
"""Math tools package."""
from .calculations import add, subtract, multiply
from .conversions import celsius_to_fahrenheit
__all__ = ["add", "subtract", "multiply", "celsius_to_fahrenheit"]
Purpose of this __init__.py: When someone does from math_tools import add, Python looks in __init__.py first. This file imports add from the submodule and makes it directly available.
Now users can write simpler code:
# Instead of:
from math_tools.calculations import add
# They can write:
from math_tools import add
When is __init__.py executed? The __init__.py file runs once when the package is first imported. If you put print statements or initialization code there, they execute at import time.
In Python 3.3+, you can also create namespace packages—directories without __init__.py files. However, for clarity and compatibility, most projects use __init__.py files explicitly.
Import Styles and Best Practices
Python gives you multiple ways to import code. Choosing the right style matters for readability and avoiding bugs.
Style 1: Import the entire module
import utils
result = utils.add(5, 3)
Pro: Clear where add comes from. Con: Requires the module prefix every time.
Style 2: Import specific items
from utils import add, subtract
result = add(5, 3)
Pro: Cleaner syntax, less typing. Con: Can be unclear where add comes from if not paying attention.
Style 3: Import with aliases
import numpy as np
import pandas as pd
from utils import add as add_numbers
result = add_numbers(5, 3)
Pro: Useful for long module names or preventing naming conflicts. Con: Requires that all code uses the alias.
Style 4: Import everything (AVOID THIS)
from utils import *
result = add(5, 3) # Where does add come from? No idea!
Pro: Minimal typing. Con: Creates ambiguity, can cause naming conflicts, makes code hard to maintain.
Here’s a comparison table of common patterns:
| Pattern | Use Case | Readability | Recommendation |
|---|---|---|---|
import module |
Simple modules you use throughout the code | Excellent | Preferred |
from module import func |
Using a few specific items frequently | Good | Preferred |
import module as alias |
Long module names, preventing conflicts | Good | Use with care |
from module import * |
Interactive sessions only | Poor | Avoid in production |
Best Practice: Use absolute imports (like from utils import func) over relative imports (like from . import func) in most cases. Absolute imports are clearer about where code comes from.
Understanding __name__ and __main__
One of the most useful but confusing features in Python is the __name__ variable. Every Python file has a special variable called __name__ that Python sets automatically.
How __name__ works: When a file runs directly (not imported), __name__ is set to "__main__". When the file is imported as a module, __name__ is set to the module’s name.
Let’s see this in action:
# demo.py
print(f"Module name: {__name__}")
def greet():
return "Hello from demo.py"
if __name__ == "__main__":
print("This code only runs when demo.py is executed directly.")
print(greet())
else:
print("This code runs when demo.py is imported as a module.")
Output (when you run demo.py directly):
Module name: __main__
This code only runs when demo.py is executed directly.
Hello from demo.py
Output (when you import it from another file):
# another_file.py
import demo
# This prints:
# Module name: demo
# This code runs when demo.py is imported as a module.
This pattern is invaluable. It lets your module do two things: define functions for others to use AND include tests or example code that only runs when you execute the file directly.
# calculations.py
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract two numbers."""
return a - b
if __name__ == "__main__":
# Test the functions
print(f"5 + 3 = {add(5, 3)}")
print(f"5 - 3 = {subtract(5, 3)}")
Output (when calculations.py runs directly):
5 + 3 = 8
5 - 3 = 2
Now, calculations.py can be imported by other files (the tests won’t run), or executed directly to test itself. This is why professional Python code always includes the if __name__ == "__main__": guard.
Understanding __file__
Another special variable is __file__, which contains the path to the current Python file. This is surprisingly useful for finding files relative to your module.
When you need to load a data file or configuration that lives next to your module, __file__ helps you find it:
# data_loader.py
import os
import json
def load_config():
"""Load configuration from a JSON file next to this module."""
current_dir = os.path.dirname(__file__)
config_path = os.path.join(current_dir, "config.json")
with open(config_path) as f:
return json.load(f)
if __name__ == "__main__":
config = load_config()
print(f"Loaded config: {config}")
Output (with config.json in the same directory):
Loaded config: {'api_key': 'secret123', 'timeout': 30}
Without __file__, finding relative paths becomes a nightmare. Different working directories would break your code. With __file__, your module is portable—it finds files relative to itself, not to wherever the user ran the script.
Common Pitfalls and How to Avoid Them
Even experienced developers hit these snags. Understanding them saves hours of debugging.
Pitfall 1: Circular Imports occur when Module A imports Module B, and Module B imports Module A. Python can’t resolve this circular dependency:
# module_a.py
from module_b import function_b
def function_a():
return function_b()
And then module_b.py tries to import from module_a in return:
# module_b.py
from module_a import function_a # Circular!
def function_b():
return function_a()
Solution: Restructure your code so dependencies flow in one direction. Move shared code to a third module that both can import from, or delay the import until it’s actually needed inside a function.
Pitfall 2: Shadowing Built-in Modules happens when your module name matches Python’s built-in modules:
# DON'T create a file named "string.py" or "json.py" in your project.
# Python will import YOUR file instead of the built-in module.
# string.py (your file - BAD IDEA)
def process():
return "My string module"
Solution: Use descriptive names that won’t conflict. Instead of string.py, use string_utils.py.
Pitfall 3: Confusion Between Relative and Absolute Imports happens when working with packages:
# Inside mypackage/module_a.py - RELATIVE IMPORT
from . import module_b # Import from the same package
# Inside mypackage/module_a.py - ABSOLUTE IMPORT
from mypackage import module_b # Full path from root
Guideline: Use absolute imports in most cases; they’re clearer and more portable. Use relative imports sparingly, only when you have a good reason.
Pitfall 4: Importing From a Directory Not in sys.path happens when Python can’t find your module:
# This fails if utils/ isn't in Python's search path
from utils import helper # ModuleNotFoundError!
Solution: Use proper package structure with __init__.py files, or add directories to sys.path if needed (though this is a code smell).
Real-Life Example: Building a Modular Weather Dashboard
Let’s put everything together. Here’s a complete project that demonstrates proper organization:
# Project structure:
# weather_dashboard/
# ├── main.py
# ├── data/
# │ ├── __init__.py
# │ └── sources.py
# └── display/
# ├── __init__.py
# └── formatters.py
Let’s build each file. First, the data source module that fetches weather information:
# data/sources.py
"""Weather data sources."""
import random
from datetime import datetime
def fetch_weather(location):
"""Simulate fetching weather data."""
return {
"location": location,
"temperature_c": random.randint(10, 30),
"humidity": random.randint(40, 90),
"condition": random.choice(["Sunny", "Cloudy", "Rainy"]),
"timestamp": datetime.now().isoformat()
}
The data/sources.py module handles fetching weather data (simulated here with random values). In a real project, this would call a weather API. Now let’s create the display formatter:
# display/formatters.py
"""Format weather data for display."""
def celsius_to_fahrenheit(celsius):
"""Convert temperature."""
return (celsius * 9/5) + 32
def format_weather_report(data):
"""Create a formatted weather report."""
fahrenheit = celsius_to_fahrenheit(data["temperature_c"])
report = f"""
╔════════════════════════════════════╗
║ Weather Report for {data['location']:<18} ║
╠════════════════════════════════════╣
║ Temperature: {data['temperature_c']}°C ({fahrenheit:.1f}°F) ║
║ Humidity: {data['humidity']}% ║
║ Condition: {data['condition']:<25} ║
║ Updated: {data['timestamp']:<23} ║
╚════════════════════════════════════╝
"""
return report
The formatter converts temperatures and builds a clean text-based report. Next, set up the __init__.py files to control each package's public interface:
# data/__init__.py
"""Data package for weather sources."""
from .sources import fetch_weather
__all__ = ["fetch_weather"]
The data/__init__.py re-exports fetch_weather so users can import directly from the package. Do the same for the display package:
# display/__init__.py
"""Display package for formatting weather information."""
from .formatters import format_weather_report
__all__ = ["format_weather_report"]
With the __init__.py files in place, the main script can use clean, simple imports from each package:
# main.py
"""Weather Dashboard - Main Entry Point."""
from data import fetch_weather
from display import format_weather_report
def main():
"""Run the weather dashboard."""
locations = ["London", "New York", "Tokyo", "Sydney"]
for location in locations:
weather = fetch_weather(location)
report = format_weather_report(weather)
print(report)
if __name__ == "__main__":
main()
Output (when you run main.py):
╔════════════════════════════════════╗
║ Weather Report for London ║
╠════════════════════════════════════╣
║ Temperature: 22°C (71.6°F) ║
║ Humidity: 65% ║
║ Condition: Sunny ║
║ Updated: 2026-03-14T14:32:18.123456 ║
╚════════════════════════════════════╝
╔════════════════════════════════════╗
║ Weather Report for New York ║
╠════════════════════════════════════╣
║ Temperature: 18°C (64.4°F) ║
║ Humidity: 72% ║
║ Condition: Cloudy ║
║ Updated: 2026-03-14T14:32:18.234567 ║
╚════════════════════════════════════╝
(Output continues for Tokyo and Sydney with randomized values...)
This example demonstrates several key concepts: modules organized into packages, __init__.py files controlling the public interface, absolute imports throughout, and a clear separation of concerns (data fetching vs. display formatting). When your dashboard grows to 50 functions, this organization keeps everything manageable.
Frequently Asked Questions
Should I organize my imports in any particular order?
Yes, PEP 8 (Python's style guide) recommends grouping imports: standard library first, third-party packages second, and local modules last, with blank lines between groups. Example:
import os
import sys
import requests
import numpy as np
from myproject import utils
from myproject.data import loader
When should I use relative imports like from . import module?
Use relative imports only within packages when you have a good reason (like avoiding name collisions). For most projects, absolute imports are clearer. Relative imports can break if your package structure changes or if someone runs the module in unexpected ways.
Can I leave __init__.py completely empty?
Yes, an empty __init__.py is perfectly valid and commonly used. Python just needs the file to exist to recognize a directory as a package. However, it's often helpful to put documentation, import statements, or initialization code there.
Are modules imported multiple times if I import them in multiple files?
No. Python caches imported modules in sys.modules. The first import runs the module's code, but subsequent imports return the cached version. This is efficient and prevents re-execution.
I have circular dependencies; how do I really fix them?
The best solution is restructuring: move shared code to a separate module that both modules import. If that's not feasible, delay the import until inside the function that needs it (import at the bottom of the function, not at the top). Example:
def my_function():
from another_module import some_func # Import only when needed
return some_func()
How do I properly import my modules when running tests from a different directory?
Use absolute imports with your project as the root. If you have a project structure with packages, install your project in development mode using pip install -e . (with a setup.py), or ensure your test runner is aware of the project root.
Conclusion
Splitting Python code into multiple files is not about complexity—it's about clarity. A well-organized project with modules and packages is easier to understand, test, extend, and collaborate on. The patterns you've learned here—modules, packages, __init__.py, import styles, and the __name__ variable—are the foundation of every professional Python project, from small scripts to massive frameworks.
Start with simple modules in the same directory. As your project grows, organize them into packages. Use __init__.py to control your public interface. Follow import best practices. Your future self (and your teammates) will thank you. For more details on Python's module system, check the official Python documentation on modules and packages.
Thanks for your good way of teaching. I felt like I’ve been with a tutor physically.
Thank you for the kind words Joel – let us know if there are other topics you’d like us to cover
Charles
This was really helpful. Thanks a lot!