What you'll learn
- Orient yourself in an unfamiliar project in minutes: entry points, structure, tests, and configuration
- Trace one request or command from entry to result using search,
inspect, and the debugger - Read code faster than you write it by knowing what to skip
- Leave a codebase better than you found it with a small, safe first change
Concept
Professional programming is mostly reading. You join a project with tens of thousands of lines, a bug report, and a day. The skill is not understanding everything; it is finding the ten percent that matters for the task and trusting the rest until proven otherwise. That skill has a method, and it uses tools you already have: the project's own layout conventions, grep, the test suite, Git history, and a debugger.

Start outside. The README says what the project does and how to run it. pyproject.toml names the entry points, [project.scripts], the dependencies, which tell you the frameworks in play, and the tool settings, which tell you the team's standards. The folder layout of src tells you the concerns: api, models, services, cli. Run the tests before reading any code; a green suite tells you the environment works, and the test names are a table of contents of what the code promises. git log --oneline | head and git shortlog -sn tell you how active the project is and who knows it.
| Question | Where to look |
|---|---|
| what does it do, how do I run it? | README, pyproject.toml scripts, Dockerfile, compose.yaml |
| where does execution start? | [project.scripts], __main__.py, if __name__ == "__main__", app factories |
| how is it organized? | src/<pkg>/ folder names, __init__.py exports |
| what does it promise? | test names; run pytest -v |
| why is this line here? | git blame, then git show on the commit; the PR discussion |
| who calls this function? | grep -rn "name(" src tests or the editor's references |
| what does this object hold at runtime? | breakpoint() in a test; vars(obj), type(obj).__mro__ |
Then go inside along one path. Pick a single behavior, one command or one request, find where it enters, and follow it with search: the function name, then the names it calls, reading only the bodies on the path and skipping every branch that does not fire for your case. When a call disappears into a framework or a decorator, put a breakpoint() in a test that exercises the path and inspect the live objects: vars(), type(), __mro__, inspect.getsource. The debugger answers "what is this really" faster than any amount of reading.
Knowing what to skip is the speed. Skip configuration boilerplate, skip generated code, skip tests that are not about your path, skip any function whose name and docstring you believe until something contradicts them. Read signatures and docstrings before bodies. Read the tests for a module before the module. Draw the three or four objects involved and the arrows between them on paper; if you cannot, you have not found the path yet.
The first change in a new codebase should be small, safe, and complete: a test that documents behavior you had to discover, a docstring where one was missing, a type hint that clarifies. It proves you can run, change, test, and submit in this project, it leaves a trace for the next reader, and it earns the trust for the larger change. Reading and contributing are the same skill at different depths, and both improve by doing them on real projects, which the closing lessons will point you toward.

Code
Orientation, automated: a script that reads a project's pyproject.toml, entry points, package layout, and test names, which is the first five minutes of any new codebase.
# orient.py
import tomllib
from pathlib import Path
root = Path("sample_project")
(root / "src/shop/api").mkdir(parents=True, exist_ok=True)
(root / "src/shop/models").mkdir(exist_ok=True)
(root / "tests").mkdir(exist_ok=True)
(root / "pyproject.toml").write_text('''
[project]
name = "shop"
version = "2.3.0"
dependencies = ["httpx>=0.28", "sqlalchemy>=2.0"]
[project.scripts]
shop = "shop.cli:main"
shop-worker = "shop.worker:run"
''')
(root / "src/shop/__init__.py").write_text("")
(root / "src/shop/cli.py").write_text("def main():\n return 0\n")
(root / "src/shop/worker.py").write_text("def run():\n return 0\n")
(root / "src/shop/api/routes.py").write_text("def get_order(order_id):\n return {}\n")
(root / "src/shop/models/order.py").write_text("class Order:\n pass\n")
(root / "tests/test_orders.py").write_text(
"def test_order_total_includes_tax(): pass\ndef test_order_refund_restores_stock(): pass\n"
)
(root / "tests/test_cli.py").write_text("def test_cli_exit_code_zero(): pass\n")
meta = tomllib.loads((root / "pyproject.toml").read_text())["project"]
print("project:", meta["name"], meta["version"], "| depends on:", [d.split(">=")[0] for d in meta["dependencies"]])
print("entry points:", meta["scripts"])
print("concerns:", sorted(p.name for p in (root / "src/shop").iterdir() if p.is_dir()))
tests = [ln.split("def ")[1].split("(")[0]
for f in (root / "tests").glob("test_*.py")
for ln in f.read_text().splitlines() if ln.startswith("def test_")]
print("promises:", sorted(t.removeprefix("test_").replace("_", " ") for t in tests))
project: shop 2.3.0 | depends on: ['httpx', 'sqlalchemy']
entry points: {'shop': 'shop.cli:main', 'shop-worker': 'shop.worker:run'}
concerns: ['api', 'models']
promises: ['cli exit code zero', 'order refund restores stock', 'order total includes tax']Tracing a path with search. Starting from a command name, find the entry, then each callee, reading only what is on the path. A small grep helper stands in for your editor's "find references".
# trace.py
import re
from pathlib import Path
root = Path("trace_project")
root.mkdir(exist_ok=True)
(root / "cli.py").write_text("from service import refund\n\ndef main(argv):\n return refund(int(argv[0]))\n")
(root / "service.py").write_text(
"from repo import load_order, save_order\nfrom stock import restore\n\n"
"def refund(order_id):\n order = load_order(order_id)\n restore(order.items)\n"
" order.status = 'refunded'\n save_order(order)\n return order\n"
)
(root / "repo.py").write_text("def load_order(order_id): ...\ndef save_order(order): ...\n")
(root / "stock.py").write_text("def restore(items): ...\n")
(root / "unrelated.py").write_text("def refund_policy_text(): return 'not the same refund'\n")
def definitions(name):
return [f"{p.name}:{i}" for p in root.glob("*.py") for i, ln in enumerate(p.read_text().splitlines(), 1)
if re.match(rf"^def {name}\(", ln)]
def callees(name):
src = next(p for p in root.glob("*.py") if re.search(rf"^def {name}\(", p.read_text(), re.M))
body = src.read_text().split(f"def {name}(")[1].split("\ndef ")[0]
return sorted(set(re.findall(r"\b([a-z_]+)\(", body)) - {name, "int"})
print("entry:", definitions("main"))
path = ["main"]
frontier = ["refund"]
while frontier:
fn = frontier.pop(0)
path.append(f"{fn} -> {callees(fn)}")
frontier += [c for c in callees(fn) if definitions(c) and callees(c)]
for step in path:
print(" ", step)
print("skipped:", [p.name for p in root.glob("*.py")
if "refund" in p.read_text() and p.name not in ("cli.py", "service.py")])
entry: ['cli.py:3']
main
refund -> ['load_order', 'restore', 'save_order']
skipped: ['unrelated.py']Inspecting live objects when reading is not enough: what an object holds, what class it really is, where a method was defined, and the source of a function you did not write.
# introspect.py
import inspect
from dataclasses import dataclass, field
class Base:
def describe(self):
return f"{type(self).__name__} with {len(vars(self))} fields"
@dataclass
class Order(Base):
id: int
items: list = field(default_factory=list)
status: str = "open"
order = Order(7, ["pen"])
print(vars(order))
print(type(order).__name__, [c.__name__ for c in type(order).__mro__])
print("describe defined in:", inspect.getsourcefile(Order.describe).split("/")[-1], "class", Order.describe.__qualname__.split(".")[0])
print(inspect.signature(Order))
print(inspect.getsource(Base.describe).strip().splitlines()[0])
{'id': 7, 'items': ['pen'], 'status': 'open'}
Order ['Order', 'Base', 'object']
describe defined in: introspect.py class Base
(id: int, items: list = <factory>, status: str = 'open') -> None
def describe(self):Git as a reading tool: who changed this, when, and why. A tiny repository with two commits shows blame and log -S answering "where did this line come from".
# git_archaeology.sh
rm -rf hist && mkdir hist && cd hist && git init -q -b main
git config user.name "Ada" && git config user.email "ada@example.com"
printf 'RATE = 0.20\n' > tax.py && git add tax.py && git commit -q -m "Add VAT rate"
printf 'RATE = 0.19 # reduced 2026-07, see ticket #412\n' > tax.py && git commit -q -am "Lower VAT to 19% per new regulation
Finance asked for this on 2026-07-01; ticket #412 has the source."
git blame -s tax.py | sed -E 's/^[0-9a-f]+ +/<hash> /'
echo "--- why:"
git log -1 --format=%B $(git blame -l -L 1,1 tax.py | cut -d' ' -f1) | head -1
echo "--- when RATE changed:"
git log --oneline -S"0.19" | sed -E 's/^[0-9a-f]+ /<hash> /'
<hash> 1) RATE = 0.19 # reduced 2026-07, see ticket #412
--- why:
Lower VAT to 19% per new regulation
--- when RATE changed:
<hash> Lower VAT to 19% per new regulationTry it yourself
Clone a small, active open-source Python project you use, such as a CLI tool or a library from this course's dependencies. Spend five minutes on orientation only: README, pyproject.toml, layout, pytest -v. Pick one command or function, trace it with your editor's search to the point where it does its real work, and put a breakpoint() in a test on that path to inspect the live objects. Write down the three or four objects involved. Then find one function without a docstring on that path and write one, as your first small change.
Common mistakes
- Reading top to bottom. Codebases are not books; start from the entry point of one behavior and follow calls.
- Trying to understand everything before changing anything. Depth on one path, then a small change, builds real understanding; breadth builds anxiety.
- Ignoring the tests and the history. Test names are the spec and commit messages are the reasons; both are faster than deducing from code.
What's next
- Orient outside-in: README,
pyproject.toml, layout, tests, Git history; then trace one path with search and a breakpoint. - Skip what is not on the path; read signatures, docstrings, and tests before bodies; draw the objects involved.
- Make the first change small, safe, tested, and documenting: it proves the loop works and earns trust.
You can enter any codebase. Next you will finish your own: the capstone that ships the expense library as a complete, production-ready package.