Interactive Python Reference · taskbook.py

Learn Python
through a real program

A fully functional command-line task manager annotated to teach Python syntax, idioms, and structure. Every language feature appears because it solves a real problem — not as a contrived example.

Python 3.10+ No external packages 14 core concepts 560 lines JSON persistence
Terminal
$ python --version # Confirm Python 3.10+
$ python taskbook.py # Run the program
# First run creates tasks.json in the same directory.
# Type 'a' to add a task. Type 'q' to quit.
01

Imports

Python's import system gives you access to the standard library and third-party packages. Import only what you need — clean imports document your dependencies.

import from … import standard library pathlib dataclasses typing collections

Two import styles

import json imports the whole module — you then write json.dumps() to call it. The module name acts as a namespace, so there are no name collisions.

from pathlib import Path imports just the Path class directly into your namespace. You write Path("file.txt") instead of pathlib.Path("file.txt").

Line-by-line

  • json Built-in JSON encoder/decoder — serialize Python objects to strings and back.
  • functools Higher-order functions: wraps, reduce, lru_cache, partial.
  • datetime Dates, times, and time arithmetic.
  • Path Object-oriented file-path manipulation — much cleaner than os.path.
  • dataclass Decorator that auto-generates __init__, __repr__, __eq__ from annotations.
  • Optional Type hint: Optional[str] means the value can be str or None.
  • Counter Dict subclass that auto-counts hashable objects.
No external packages. Every import here is from Python's standard library — no pip install required to run this program.
taskbook.py — lines 31–37
02

Constants & Module-Level Variables

Module-level code runs once when the file is first imported. Constants use UPPER_CASE naming by convention (PEP 8). Python has no true immutable constants — this is a social contract enforced by naming.

UPPER_CASE convention set {} dict {} tuple () dict comprehension

Three collection types in one block

This section uses three different collection types on purpose — each chosen for its properties:

set {} Unordered, unique values. O(1) membership test. Perfect for valid-choices checking: if x in PRIORITIES. dict {} Key→value mapping. O(1) lookup. Maps priority names to emoji symbols. tuple () Immutable ordered sequence. Good for fixed data like (label, code) pairs.

Dict comprehension

The last line builds a dict from the COLOURS tuple using a dict comprehension:

C = {name: code for name, code in COLOURS}

This is equivalent to a for loop that calls d[name] = code — but in one expressive line.

Gotcha: {"a", "b"} is a set. An empty {} is a dict, not a set. For an empty set, write set().
taskbook.py — lines 48–67
03

The Decorator

A decorator is a function that wraps another function to add behaviour before or after it runs — without modifying the original function's code. @log_errors catches exceptions from any method it decorates and prints them cleanly.

decorator @functools.wraps *args / **kwargs try / except closure

How a decorator works

@log_errors above def add() is syntactic sugar for:

add = log_errors(add)

The original add function is replaced by wrapper, which calls the original inside a try/except.

  • *args Collects all extra positional arguments into a tuple. The * "unpacks" them when calling the original function.
  • **kwargs Collects all keyword arguments into a dict. The ** unpacks them on call.
  • @wraps Copies __name__, __doc__ from func to wrapper. Without it, every decorated function looks like "wrapper" in tracebacks.
  • as e Binds the exception object to the name e so you can print or inspect it.
Why use a decorator here? Without it, every CRUD method (add, remove, complete…) would need its own try/except block — repeated boilerplate. The decorator removes that repetition.
See the before/after comparison

Without decorator — every method needs this pattern:

def add(self, title):
    try:
        # actual logic
        ...
    except (ValueError, KeyError) as e:
        print(f"Error: {e}")

def remove(self, idx):
    try:
        ...
    except (ValueError, KeyError) as e:
        print(f"Error: {e}")

With @log_errors — write logic only, error handling is automatic.

taskbook.py — lines 78–92
04

The Task Dataclass

@dataclass auto-generates __init__, __repr__, and __eq__ from field annotations. It's the modern Python way to write data-holding classes without boilerplate.

@dataclass field() type annotations lambda @classmethod __str__ / __repr__

Field declarations

Each line is a field: name: type = default. Fields without a default are required arguments to Task(). Fields with a default are optional.

Critical gotcha — mutable defaults:
Never write tags: list = [] in a class body. That ONE list is shared by every instance — appending to one task's tags would modify all tasks.

Correct: tags: list = field(default_factory=list)
This calls list() freshly for each new instance.

Special methods (dunders)

  • __str__Called by print(obj) and str(obj). Should return a human-readable string.
  • __repr__Called by repr(obj) and in the interactive console. Should be unambiguous and ideally eval-able.
  • @classmethodReceives the class (cls) as the first arg instead of an instance (self). Used as an alternate constructor here: Task.from_dict(data).

Lambda as default factory

lambda: datetime.datetime.now().strftime("%Y-%m-%d") is an anonymous zero-argument function. It's called each time a Task is created, capturing the current date at that moment.

taskbook.py — lines 105–141
05

The TaskBook Class

A traditional class (not a dataclass) because it needs custom __init__ logic, a computed property, a generator method, and file I/O. This is where OOP earns its place.

class / __init__ class variable vs instance variable @property generator / yield list comprehension lambda + sorted() context manager json

Class vs instance variables

_instance_count Class variable — declared directly in the class body. Shared across ALL instances. Useful for counting objects. self.filepath Instance variable — set in __init__ on self. Unique to each TaskBook object.

@property — computed attribute

book.stats looks like an attribute access but runs a function. The caller writes book.stats (not book.stats()), which makes the API cleaner and hides implementation details.

Generator — yield

pending() uses yield. This makes it a generator function. Nothing is computed until the caller iterates: for task in book.pending():. Values are produced lazily — one at a time — saving memory.

Context manager — with open()

with open(...) as f: guarantees the file is closed even if an exception occurs. It calls f.__enter__() on entry and f.__exit__() on exit.

Lambda in sorted()

sorted(self.tasks, key=lambda t: priority_order.get(t.priority, 99)) — the key= argument is called on each element to produce the value to sort by. lambda t: ... is an anonymous function: "given task t, return..."

List comprehension avoids mutation bugs:
self.tasks = [t for t in self.tasks if not t.done]
Creates a NEW list (no mutation while iterating), then replaces the old reference. Safer than list.remove() inside a loop.
taskbook.py — lines 151–310 (key methods)
06

Standalone Functions

Not everything belongs in a class. Pure functions — functions that depend only on their inputs and have no side effects — are easier to test, read, and reuse.

def / return default arguments enumerate() list comprehension f-strings or as fallback

Default arguments

def display_tasks(tasks, heading="Tasks")heading has a default value. Callers can omit it: display_tasks(book.tasks) works fine.

enumerate() — never use range(len())

for i, task in enumerate(tasks, start=1): gives both the index (1-based here) and the value. Idiomatic Python: never write for i in range(len(lst)): lst[i].

List comprehension as filter+transform

parse_tags turns "work, home , URGENT" into ['work', 'home', 'urgent'] in one line:

[t.strip().lower() for t in raw.split(",") if t.strip()]

Pattern: [expr for item in iterable if condition]

or as a default fallback

return raw or default — if raw is any falsy value (empty string, None, 0, empty list), the expression short-circuits and returns default. This is extremely common Python idiom.

F-string format specs:
f"{i:2}" — right-aligns i in a 2-character field. So 1 becomes " 1" and 10 stays "10". Keeps list numbers aligned.
taskbook.py — lines 319–379
07

The Main Loop & Dispatch Table

The main() function orchestrates everything. Two key patterns: a dispatch table (dict of functions) and match/case structural pattern matching.

while True / break dispatch table match / case guard clause first-class functions

Functions as first-class objects

In Python, functions are objects like any other. They can be stored in a dict — this is the dispatch table pattern:

dispatch = {"a": run_add, "l": run_list, ...}

Instead of a long if/elif chain, you look up and call: dispatch[cmd](book). Adding a new command only requires adding one line to the dict.

match / case (Python 3.10+)

  • case "q"Exact string match — only matches the string "q".
  • case "" | "?"OR pattern — matches either the empty string or "?".
  • case cmd if…Capture + guard. Assigns input to cmd, then checks the guard condition. Only matches when both succeed.
  • case _Wildcard — matches anything not caught above. Like default in other languages.
On Python 3.9 or earlier? Replace the match/case block with a standard if/elif/else chain. Everything else in the program is compatible with Python 3.8+.

while True / break

An infinite loop that runs until a break statement exits it. This is the standard pattern for "keep running until the user quits." The break happens inside case "q".

taskbook.py — lines 520–550 (main loop core)
08

The Script Guard

The last two lines of every Python script. This pattern ensures main() only runs when the file is executed directly, not when it's imported as a module by another script.

if __name__ == "__main__" module system __name__ dunder

How Python sets __name__

Direct run python taskbook.py → Python sets __name__ = "__main__". The guard is True, main() runs. Imported import taskbook from another file → Python sets __name__ = "taskbook". The guard is False, main() does NOT run.

Without this guard, importing taskbook anywhere would immediately launch the interactive menu — which is never what you want.

Always use this pattern. Even small scripts benefit from it: it makes the code importable (and therefore testable) without triggering side effects.

Why define main() at all?

You could put all the code at the top level without a main() function. But wrapping it in main() means all your variables are local (faster, no global namespace pollution) and the code is testable.

taskbook.py — lines 553–563

Complete Source: taskbook.py

The full 560-line program. Every concept from the sections above is annotated inside the code with inline comments.

taskbook.py — complete file

Concepts Quick Reference

Variables & f-strings
All sections

name = "Alice" — dynamic typing, no declaration needed. f"Hello {name}" — format strings embed expressions directly.

set {}
Section 02 — PRIORITIES

Unordered, unique values. O(1) membership test with in. Use when you need fast "is this value valid?" checks.

dict {}
Section 02, 07

Key→value mapping. O(1) lookup. The dispatch table is a dict of functions. d.get(k, default) avoids KeyError.

tuple ()
Section 02 — COLOURS

Immutable ordered sequence. Use for fixed data, coordinate pairs, return values. Tuples can be unpacked: name, code = pair.

Decorator
Section 03 — log_errors

@decorator replaces a function with a wrapper. @functools.wraps preserves the original's metadata. Eliminates repeated boilerplate.

@dataclass
Section 04 — Task

Auto-generates __init__, __repr__, __eq__ from field annotations. Use field(default_factory=list) for mutable defaults.

lambda
Section 04, 05

Anonymous single-expression function. lambda t: t.priority is shorthand for def f(t): return t.priority. Used as key= in sorted().

Class & __init__
Section 05 — TaskBook

self is the instance. Class variables are shared; instance variables (set on self) are unique. _name = private by convention.

@property
Section 05 — stats

A method that looks like an attribute. book.stats runs code but the caller sees no parentheses. Computed attributes hide implementation details.

Generator / yield
Section 05 — pending()

yield turns a function into a lazy generator. Values are produced one at a time when the caller iterates. Saves memory for large collections.

List comprehension
Sections 05, 06

[expr for item in it if cond] — filter + transform in one line. Creates a new list; safer than modifying a list while iterating.

Context manager
Section 05 — _save/_load

with open(...) as f: guarantees the file closes even on error. Any object with __enter__/__exit__ methods works as a context manager.

Exception handling
Section 03, 05

try / except / else / finally. Catch specific types first. Use raise to re-raise or signal invalid input. Custom exceptions inherit from Exception.

match / case
Section 07 — main()

Python 3.10+ structural pattern matching. Supports OR (|), capture patterns, and guard clauses (case x if condition). Wildcard: case _.

Common Exception Types

Exception When it occurs Example
ValueErrorRight type, wrong valueint("abc"), empty title string
KeyErrorDict key not foundd["missing"]
IndexErrorList index out of rangelst[99] on a 3-item list
TypeErrorWrong type for operation"a" + 1
AttributeErrorObject has no attributeNone.strip()
FileNotFoundErrorFile doesn't existopen("missing.txt")
json.JSONDecodeErrorInvalid JSON stringCorrupt tasks.json file
StopIterationGenerator exhaustedCalling next() past the end
PEP 8 — Python Style Rules
Indentation4 spaces. Never tabs. Python will raise IndentationError if inconsistent. Line length79 characters maximum. Use backslash \ or parentheses for continuation. Namingsnake_case for variables/functions. PascalCase for classes. UPPER_CASE for constants. Blank lines2 blank lines between top-level definitions. 1 blank line between methods. ImportsAt the top. stdlib first, then third-party, then local. Each group separated by a blank line. Auto-formatRun black taskbook.py or ruff format taskbook.py to auto-apply PEP 8.
Dunder (Magic) Methods quick reference
MethodTriggered byIn this program
__init__Object creation Task("Buy milk")TaskBook, Task
__str__str(obj) / print(obj)Task.__str__
__repr__repr(obj) / interactive consoleTask.__repr__
__eq__obj1 == obj2Auto-generated by @dataclass
__len__len(obj)Not used — len(self.tasks) instead
__enter__ / __exit__with obj:Used implicitly via open()