Phase 1: Classes & Dicts
Phase 1: Your First Class and Dicts
Objective
Build the empty skeleton of AssociationGraph — a class that holds two dictionaries: one for forward associations (A relates-to B) and one for reverse associations (B is-related-by A).
No logic yet.
Just the container and the proof it works.
Python Concepts
| Concept | Plain English |
|---|---|
|
A blueprint for creating objects.
Think of it as an AsciiDoc template: the template defines structure, and each page you generate from it is an instance.
|
|
A reference to this particular instance of the class.
When you have two graphs, |
|
The constructor method.
Python calls it automatically when you create an instance with |
|
Python’s associative array — the same concept Kernighan described in The AWK Programming Language.
A |
Type hints |
Annotations that document what a variable holds.
|
|
An ordered, mutable sequence.
|
Nested dicts |
A dict inside a dict.
|
Steps
1. Understand the data shape
Before writing code, visualize the structure. The forward dict maps an entity to its relations and targets:
{
"CISSP": {
"covers": ["access-control", "cryptography"],
"requires": ["5-years-experience"]
}
}
The reverse dict holds the inverse view:
{
"access-control": {
"covered-by": ["CISSP"]
}
}
Two dicts. Same data. Different directions of traversal.
2. Write the class
Open src/association_engine/graph.py and write:
"""Bidirectional association graph."""
class AssociationGraph:
"""A graph that stores entities, relations, and their inverses.
Internally this is two nested dicts:
_forward: {source: {relation: [targets]}}
_reverse: {target: {inverse_relation: [sources]}}
"""
def __init__(self) -> None:
# Forward: source -> relation -> [targets]
self._forward: dict[str, dict[str, list[str]]] = {}
# Reverse: target -> inverse_relation -> [sources]
self._reverse: dict[str, dict[str, list[str]]] = {}
Walk through this line by line:
-
class AssociationGraph:— declares the class. The colon starts an indented block (like{in C or awk). -
def __init__(self) → None:— the constructor.defdefines a function.selfis always the first argument of a method.→ Noneis a type hint saying this method returns nothing. -
self._forward— the underscore prefix is a Python convention meaning "private — don’t touch this from outside the class." It is not enforced, just a signal. -
dict[str, dict[str, list[str]]]— the type hint. Read it inside-out: a list of strings, inside a dict keyed by strings, inside another dict keyed by strings.
3. Export the class from the package
Open src/association_engine/__init__.py and write:
"""Association Engine — bidirectional knowledge graph."""
from association_engine.graph import AssociationGraph
__all__ = ["AssociationGraph"]
from … import … is how Python pulls a name from another module.
__all__ declares the public API — when someone writes from association_engine import *, only AssociationGraph is exported.
4. Write the first real test
Open tests/test_graph.py and replace the smoke test:
"""Tests for AssociationGraph — Phase 1."""
from association_engine.graph import AssociationGraph
class TestInit:
"""Verify the constructor creates an empty graph."""
def test_forward_starts_empty(self) -> None:
graph = AssociationGraph()
assert graph._forward == {}
def test_reverse_starts_empty(self) -> None:
graph = AssociationGraph()
assert graph._reverse == {}
def test_two_instances_are_independent(self) -> None:
a = AssociationGraph()
b = AssociationGraph()
a._forward["test"] = {}
assert "test" not in b._forward
Three tests:
-
Forward dict starts empty.
-
Reverse dict starts empty.
-
Two instances do not share state (a common beginner trap with mutable default arguments — you avoided it by assigning in
__init__).
5. Run the tests
uv run pytest tests/ -v
Expected:
tests/test_graph.py::TestInit::test_forward_starts_empty PASSED
tests/test_graph.py::TestInit::test_reverse_starts_empty PASSED
tests/test_graph.py::TestInit::test_two_instances_are_independent PASSED
6. Lint
uv run ruff check src/ tests/
Fix anything ruff reports before moving on. Common first-time issues: missing trailing newline, unused imports.
Checklist
-
AssociationGraphclass defined ingraph.py -
__init__creates empty_forwardand_reversedicts -
Type hints on both dicts
-
__init__.pyexportsAssociationGraph -
Three tests in
test_graph.py -
uv run pytest tests/ -v— all pass -
uv run ruff check src/ tests/— clean
Verification
uv run pytest tests/ -v --tb=short
uv run ruff check src/ tests/
Both must exit cleanly. You now have a class that does nothing useful — but it exists, it is tested, and the toolchain proves it. Phase 2 gives it behavior.