Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
A decorator gives a callable a coat
A function is a callable: an object you can call with (). A decorator is ordinary Python that receives a callable and returns the callable people will use.
- Original call the real job
- Wrapper policy around the job
- Caller uses the wrapped callable
def announce(function):
def wrapper(*args, **kwargs):
print("before")
result = function(*args, **kwargs)
print("after")
return result
return wrapper
@announce
def greeting(name):
return f"Hello, {name}"
print(greeting("Mina"))
Output:
before
after
Hello, Mina
@announce means greeting = announce(greeting). Decoration happens when Python executes the function definition; the wrapper runs later on each call. A transparent wrapper forwards *args and **kwargs and returns the original result. Forgetting return result quietly gives callers None.
Keep the name and configure the coat
An ordinary wrapper is named wrapper, not greeting. functools.wraps() preserves the original name, documentation, annotations, and __wrapped__ link.
- Name useful tracebacks
- Docstring help still works
- __wrapped__ points to the original
A configurable decorator is a decorator factory with three small layers:
from functools import wraps
def tagged(label): # receive configuration
if not label:
raise ValueError("label must not be empty")
def decorate(function): # receive the callable
@wraps(function)
def wrapper(*args, **kwargs): # receive call arguments
return f"[{label}] {function(*args, **kwargs)}"
return wrapper
return decorate
@tagged("study")
def total(a, b):
"""Add two study times."""
return a + b
print(total(20, 15))
print(total.__name__)
Output:
[study] 35
total
tagged("study") and decorate(total) run at definition time. The inner wrapper runs at call time. If decorators are stacked, the one nearest the function is applied first; use stacks only when you can explain their order plainly.
with opens and closes a doorway
A context manager surrounds a block rather than one call. with calls __enter__(), runs the block, and always calls __exit__() afterward.
- __enter__ acquire and return a value
- with block use the managed thing
- __exit__ release even after failure
class Notebook:
def __enter__(self):
print("open")
self.notes = []
return self.notes
def __exit__(self, exc_type, exc_value, traceback):
print("close")
return False
with Notebook() as notes:
notes.append("context managers")
print(notes)
Output:
open
['context managers']
close
The value returned by __enter__() becomes notes. On success, the three exception arguments to __exit__() are None; after failure, they describe the exception. A truthy return suppresses that exception. Returning False or None lets it continue. Suppression must be a narrow, documented choice, never an accidental True.
contextlib makes one doorway smaller
For one simple entry and exit path, contextlib.contextmanager turns a generator into a context manager:
from contextlib import contextmanager
@contextmanager
def section(name):
print("open", name)
try:
yield []
finally:
print("close", name)
with section("testing") as notes:
notes.append("arrange-act-assert")
print(notes)
Output:
open testing
['arrange-act-assert']
close testing
Code before yield enters; the yielded value belongs to the block; finally guarantees cleanup. If the block fails, its exception arrives at the yield and normally keeps travelling after cleanup. Catch broadly only when the manager has a clear audit or rollback policy, then use bare raise to preserve the original traceback.
Short boundaries: close only resources the manager owns, prefer a resource’s native with support, and create a fresh generator-based manager for each with because one instance is single-use.
Build a transactional reading journal
Save this complete standard-library project as reading_journal.py:
from contextlib import contextmanager
from dataclasses import dataclass, field
from functools import wraps
@dataclass
class Journal:
entries: list[str] = field(default_factory=list)
audit: list[str] = field(default_factory=list)
def audited(audit, label):
def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
audit.append(f"{label}:start")
try:
result = function(*args, **kwargs)
except Exception as error:
audit.append(f"{label}:error:{type(error).__name__}")
raise
else:
audit.append(f"{label}:ok")
return result
return wrapper
return decorate
@contextmanager
def staged(journal):
pending = []
journal.audit.append("batch:open")
try:
yield pending
except Exception:
journal.audit.append("batch:rollback")
raise
else:
journal.entries.extend(pending)
journal.audit.append(f"batch:commit:{len(pending)}")
finally:
journal.audit.append("batch:close")
def parse_entry(line):
topic, separator, minutes_text = line.partition("|")
if not separator or not topic.strip():
raise ValueError("expected topic|minutes")
try:
minutes = int(minutes_text)
except ValueError as cause:
raise ValueError("minutes must be an integer") from cause
if minutes <= 0:
raise ValueError("minutes must be positive")
return f"{topic.strip()} ({minutes} min)"
def make_importer(journal):
@audited(journal.audit, "import")
def import_lines(lines):
with staged(journal) as pending:
for line in lines:
pending.append(parse_entry(line))
return len(pending)
return import_lines
journal = Journal()
import_lines = make_importer(journal)
count = import_lines(["Decorators | 25", "Contexts | 30"])
saved = journal.entries.copy()
print("Imported:", count)
try:
import_lines(["Valid | 10", "Broken | many"])
except ValueError as error:
print("Rejected:", error)
assert journal.entries == saved
assert import_lines.__name__ == "import_lines"
print("Entries:", journal.entries)
print("Last audit:", journal.audit[-5:])
Run python3 reading_journal.py:
Imported: 2
Rejected: minutes must be an integer
Entries: ['Decorators (25 min)', 'Contexts (30 min)']
Last audit: ['import:start', 'batch:open', 'batch:rollback', 'batch:close', 'import:error:ValueError']
The decorator owns call-level audit events. The context manager owns pending data, commit, rollback, and cleanup. The broad catches have one documented job and immediately re-raise; the failed batch cannot change existing entries.
Three tiny missions
- Decorate a function with a docstring and verify its result,
__name__, and__doc__. - Make
@repeat(times)reject zero during decoration and call a function exactly three times. - Change
Notebook.__exit__()to suppress onlyKeyError, then prove thatValueErrorstill escapes.
Ready for Chapter 19?
- I know a decorator is an ordinary callable transformation.
- My wrapper forwards arguments, returns results, and uses
@wraps. - I can explain the three layers of a decorator factory.
- I know what
__enter__()returns and when__exit__()runs. - I put unconditional cleanup in
finallyand choose exception suppression deliberately. - I ran the journal and proved that a failed batch rolls back.
Next, you will collect three kinds of evidence with tests, a debugger, and carefully configured logs.