How Python Code Is Organized

Updated

September 8, 2026

How Python Code Is Organized

Python code lives in files. You run a file, or you import it. The boring default is a flat folder: a few .py files next to pyproject.toml. Do not start with a src/ tree or a package named after your company.

Mental model

Three words, used precisely:

  • A script is a file you run (uv run python main.py).
  • A module is a file you import (import greet). Every .py file is a module.
  • A package is a directory of modules (usually with an __init__.py). You need one when a single folder of files is no longer a clear boundary.

When Python loads a file, it sets a string named __name__. If that file is the program, __name__ is "__main__". If another file imported it, __name__ is the module name ("greet"). The usual guard means “run main only when this file is the program.”

Flat layout (boring):

desk/
  pyproject.toml
  greet.py
  main.py

src/ layout (later, when you install the package):

desk/
  pyproject.toml
  src/
    desk/
      __init__.py
      greet.py

Stay flat until a name collides or you are shipping an installable library.

Worked examples

Case 1: One file is a script and a module

Save as greet.py. It defines a function. It also has a main guard so you can run it by itself.

# greet.py
def desk_hello(name):
    return f"hello, {name}"


def main():
    print(desk_hello("desk"))


if __name__ == "__main__":
    main()

Run:

uv run python greet.py

Output:

hello, desk

Case 2: A second file imports the first

Save as main.py in the same folder as greet.py.

# main.py
import greet


def main():
    print(greet.desk_hello("nights"))
    print("greet.__name__ =", greet.__name__)
    print("this file __name__ =", __name__)


if __name__ == "__main__":
    main()

Run:

uv run python main.py

Output:

hello, nights
greet.__name__ = greet
this file __name__ = __main__

import greet loads greet.py once, binds the module to the name greet, and does not call greet.main, because inside greet.py the name is "greet", not "__main__". That is why the guard exists.

The same folder still has pyproject.toml:

# pyproject.toml
[project]
name = "desk"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

Python finds greet because the directory of the script you ran is on the import path. You did not need src/.

Case 3: __name__ is a string you can print

Save as whoami.py.

# whoami.py
def main():
    print(__name__)


if __name__ == "__main__":
    main()

Run as a script:

uv run python whoami.py

Output:

__main__

Load it as a module with -m (the next part covers -m more fully). From the same folder:

uv run python -c "import whoami; print(whoami.__name__)"

Output:

whoami

Same file, two jobs. Keep the guard so those jobs stay distinct.

Case 4: A package is a directory, not a personality

When you eventually need a package, it is a folder of modules. The boring empty __init__.py is enough:

desk/
  pyproject.toml
  desk/
    __init__.py
    greet.py

desk/__init__.py can be empty. import desk.greet then works after the package is on the path. You do not need this for the examples in this chapter. Put greet.py next to main.py until a third file makes the boundary obvious.

The trap

Work at import time. Save as noisy.py:

# noisy.py
print("loading noisy")


def ping():
    return "pong"

Save as use_noisy.py:

# use_noisy.py
import noisy


def main():
    print(noisy.ping())


if __name__ == "__main__":
    main()

Run:

uv run python use_noisy.py

Output:

loading noisy
pong

The print in noisy.py ran because import executes the file. A second import of noisy in the same process would not print again (modules are cached), which makes the surprise even harder to see. Put work in functions. Call those functions from main.

The other common trap is the opposite: a src/desk/core/utils/helpers tree on day one, with one function in it. Names collide later, not on line one. Split files when you have a reason.

The boring rule

  • One folder, several .py files, a pyproject.toml. Flat until you need a package.
  • import name for a sibling module. Call name.function.
  • Keep if __name__ == "__main__": on every file you run.
  • Do not do real work (prints, network, files) at module top level.
  • Do not use from greet import *.
  • Do not start a src/ layout until you are packaging something you install.

Try this

  1. In greet.py, add desk_bye(name) that returns f"bye, {name}". Call it from main.py.
  2. Run uv run python greet.py again after Case 2. Confirm it still prints hello, desk and does not print the lines from main.py.
  3. Move the print("loading noisy") in noisy.py into ping. Run use_noisy.py and notice import is now silent.