Multi-File Projects

The Course
January 11, 2026
5 min read

A single file is fine up to a few hundred lines. Past that, finding anything becomes the hard part. Splitting a program into modules is mostly about making it navigable.

Every playground on this page holds several files — use the tabs above the editor to move between them. Run always executes the entry file, marked with a dot.

Two files

A module beside the program Python
from tax import with_tax, TAX_RATE prices = [19.99, 4.50, 120.00] print(f"Tax rate: {TAX_RATE:.0%}") for price in prices: print(f"{price:8.2f} -> {with_tax(price):8.2f}") """Tax calculations, in one place.""" TAX_RATE = 0.2 def with_tax(amount, rate=TAX_RATE): """Return amount including tax, rounded to cents.""" return round(amount * (1 + rate), 2) def without_tax(amount, rate=TAX_RATE): """Return the pre-tax amount of a tax-inclusive price.""" return round(amount / (1 + rate), 2)

from tax import with_tax looks for tax.py in the same folder. Everything defined at the top level of that file — functions, classes, constants — is importable.

Packages

A folder with an __init__.py is a package: a module that contains other modules. It lets you group related files and gives the group a name.

A package with three modules Python
from shop import Cart from shop.pricing import with_tax cart = Cart("Ada") cart.add("keyboard", 49.99) cart.add("mouse", 25.50) print(cart) print(f"Subtotal: {cart.subtotal():.2f}") print(f"With tax: {with_tax(cart.subtotal()):.2f}") """The shop package. Re-exporting Cart here lets callers write `from shop import Cart` instead of `from shop.cart import Cart`. """ from .cart import Cart __all__ = ["Cart"] from dataclasses import dataclass, field @dataclass class Cart: customer: str items: list = field(default_factory=list) def add(self, name, price): self.items.append((name, price)) def subtotal(self): return sum(price for _, price in self.items) def __str__(self): return f"Cart({self.customer}, {len(self.items)} items)" TAX_RATE = 0.2 def with_tax(amount, rate=TAX_RATE): return round(amount * (1 + rate), 2)

Note from .cart import Cart inside __init__.py — the leading dot means “from this package”, a relative import. Use relative imports between modules of the same package, and absolute imports (from shop import Cart) from outside it.

The main guard

Code at the top level of a module runs when it is imported. That is fine for definitions, and a problem for anything that acts.

Import should not run your program Python
import greeter print("main.py is running") print(greeter.greet("Ada")) def greet(name): return f"Hello, {name}!" print("this line runs the moment greeter is imported") if __name__ == "__main__": # Only when greeter.py is the file being executed. print("greeter.py is being run directly") print(greet("world"))

Run it: the unguarded print fires during the import, while the guarded block does not. __name__ is "__main__" in the file you executed and the module’s own name everywhere else. Put your script’s actual work behind that guard, so the file can be both imported and run.

Circular imports

If a.py imports b.py and b.py imports a.py, Python cannot finish either.

The error you will eventually meet Python
import order print(order.describe()) from customer import Customer def describe(): return f"order for {Customer('Ada').name}" from order import describe class Customer: def __init__(self, name): self.name = name

The fix is almost never a clever import trick — it is to notice that the two modules are really one concern split badly, or that a third module should hold what they share.

A layout that scales

myproject/
├── README.md
├── requirements.txt
├── src/
│   └── shop/
│       ├── __init__.py      # what the package exposes
│       ├── cart.py          # one concern per module
│       ├── pricing.py
│       └── cli.py           # the entry point, behind a main guard
└── tests/
    ├── test_cart.py
    └── test_pricing.py

Two rules carry most of the weight: one concern per module, and dependencies point one waycli imports cart, cart never imports cli.

Exercise

Split a program up

The single file below does three things at once. Split it into temperature.py (the conversions), report.py (the formatting) and main.py (the entry point, behind a main guard). Use the file tabs to add code to each.

# Move the conversions to temperature.py and the formatting to report.py, # then import them here. def to_fahrenheit(celsius): return celsius * 9 / 5 + 32 def to_kelvin(celsius): return celsius + 273.15 def format_row(celsius): return f"{celsius:6.1f}C {to_fahrenheit(celsius):6.1f}F {to_kelvin(celsius):6.1f}K" for c in [-40, 0, 21.5, 100]: print(format_row(c)) # Your conversions here # Your formatting here
Show one solution

temperature.py holds the pure calculations, report.py turns numbers into text, and main.py decides what to do — each importable without side effects:

# temperature.py
def to_fahrenheit(celsius):
    return celsius * 9 / 5 + 32

def to_kelvin(celsius):
    return celsius + 273.15
# report.py
from temperature import to_fahrenheit, to_kelvin

def format_row(celsius):
    return f"{celsius:6.1f}C {to_fahrenheit(celsius):6.1f}F {to_kelvin(celsius):6.1f}K"

def format_table(values):
    return "\n".join(format_row(c) for c in values)
# main.py
from report import format_table

def main():
    print(format_table([-40, 0, 21.5, 100]))

if __name__ == "__main__":
    main()

Dependencies point one way — mainreporttemperature — so temperature.py can be tested without anything else existing.

What you learned

  • Any .py file beside yours is importable as a module.
  • A folder with __init__.py is a package; from .module import X is a relative import.
  • if __name__ == "__main__": separates “imported” from “run directly”.
  • Circular imports mean the split is wrong, not that you need a trick.
  • One concern per module, and dependencies pointing one way.
Last updated on January 11, 2026

Was this article helpful?

Your response is saved on this device.