How Python Code Is Organized
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.pyfile 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.pyOutput:
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.pyOutput:
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.pyOutput:
__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.pyOutput:
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
.pyfiles, apyproject.toml. Flat until you need a package. import namefor a sibling module. Callname.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
- In
greet.py, adddesk_bye(name)that returnsf"bye, {name}". Call it frommain.py. - Run
uv run python greet.pyagain after Case 2. Confirm it still printshello, deskand does not print the lines frommain.py. - Move the
print("loading noisy")innoisy.pyintoping. Runuse_noisy.pyand notice import is now silent.