Skip to content

English · Español

Lab 00 — Value skeleton

Goal: write the Value class skeleton: constructor, repr, the _prev/_op/_backward plumbing, and the backward() traversal. No ops yet. This lab makes the topology explicit before any math runs.

Estimated time: 90–120 minutes.

Prereqs: read theory/00..04. Phase 6 utilities exist.


What you produce

  1. src/minigrad/scalar.py containing the Value class skeleton:
  2. __init__(self, data: float, _prev: tuple = (), _op: str = "") -> None
  3. __repr__
  4. data: float, grad: float, _prev: tuple[Value, ...], _op: str, _backward: Callable[[], None]
  5. backward(self) -> None — topological sort + reverse traversal
  6. src/minigrad/scalar/BLUEPRINT.md — already pre-written by Claude. Read it now. Open it in a split with scalar.py.
  7. tests/test_scalar_skeleton.py — three tests that pass even with no ops implemented:
  8. Value(2.5).data == 2.5
  9. repr(Value(2.5)) contains "2.5"
  10. Value(2.5).backward() runs without error and sets grad to 1.0
  11. No experiments/ directory yet — labs 01 and 02 produce experiments.

TODOs

Block A — read BLUEPRINT first

  • Open src/minigrad/scalar/BLUEPRINT.md. Read the "API surface" and "anti-goals" sections fully. Disagreements? Flag them in your /phase-checkpoint; do not silently diverge.

Block B — class skeleton

  • Create src/minigrad/__init__.py (empty or with __all__ = ["Value"] after step C).
  • Create src/minigrad/scalar.py. Add the Value class.
  • Constructor takes data: float, _prev: tuple[Value, ...] = (), _op: str = "". Initialize self.data = float(data), self.grad = 0.0, self._prev = _prev, self._op = _op, self._backward = lambda: None.
  • __repr__ returns f"Value(data={self.data:.4f}, grad={self.grad:.4f})" or similar — your call, but make it informative.
  • Add type hints on all attributes and methods. mypy --strict must pass.
  • No ops yet. Resist the temptation.

Block C — backward()

  • Implement backward(self):
  • Build the topo order. Use a recursive helper or an iterative stack — your call. Doc the choice.
  • Set self.grad = 1.0 (seed).
  • Walk reversed topo, calling v._backward() for each v.
  • Defensive: assert self._backward is callable. (It always is, but the assertion documents the invariant.)
  • Decide and document: does backward() reset gradients first, or assume the caller has zeroed them? Phase 7 default: does NOT reset. The caller (Phase 9's optimizer eventually) is responsible for zero_grad(). Note this in the docstring.

Block D — type and lint

  • mypy --strict src/minigrad/scalar.py — green.
  • ruff check src/minigrad/scalar.py — green.
  • ruff format src/minigrad/scalar.py — applied.

Block E — skeleton tests

  • tests/test_scalar_skeleton.py:
    def test_construction():
        v = Value(2.5)
        assert v.data == 2.5
        assert v.grad == 0.0
    
    def test_repr():
        v = Value(2.5)
        assert "2.5" in repr(v)
    
    def test_backward_on_leaf():
        v = Value(2.5)
        v.backward()
        assert v.grad == 1.0
    

Constraints

  • No NumPy. scalar.py operates on Python float only. import math is allowed for exp, log, tanh in later labs.
  • No PyTorch in src/. PyTorch is a test-only dep.
  • Callable[[], None] is the right type for _backward. Use from collections.abc import Callable.
  • _prev as a tuple, not a list. Tuples are hashable and signal "fixed at construction".
  • No mutation of _prev after construction. Make it _prev: tuple[Value, ...] (immutable).

Stop conditions

Done when:

  1. mypy --strict and ruff both green on scalar.py.
  2. All three skeleton tests pass.
  3. You can from minigrad.scalar import Value in a Python shell and instantiate Value(2.0).

Pitfalls

  • Forward reference for type hint. _prev: tuple["Value", ...] (string-quoted) because the class is mid-definition when the annotation is read. Or use from __future__ import annotations at the top of the file.
  • Mutable default argument. Don't write _prev: tuple = () and then mutate it. Tuples are immutable so this is fine, but if you accidentally make it a list, you've made the Python "default mutable arg" bug.
  • self.grad: float = 0.0. Use 0.0, not 0. Otherwise NumPy/Python int promotion could surprise you in tests.
  • Topo sort recursion depth. Python's default recursion limit is ~1000. For Phase 7 graphs (tens of nodes), no problem. Phase 18+ training loops will need an iterative topo sort or sys.setrecursionlimit(...). Document but don't optimize yet.

When to consult solutions/

After all three skeleton tests pass. Then solutions/00-value-skeleton-ref.md (at phase open) shows the reference structure and any style choices to consider before lab 01.


Next lab: lab/01-implement-ops.md.