Annotations & Decorators — Tasks & Exercises¶
Topic: Annotations & Decorators Focus: Hands-on exercises that force the distinction between inert metadata (annotations/attributes) and live wrapping code (decorators), and exercise both processing paths — compile-time and runtime.
Table of Contents¶
- How to Use This Page
- Warm-Up
- Python Decorator Tasks
- Java Annotation Tasks
- TypeScript / C# Tasks
- Cross-Cutting & Trap Tasks
- Stretch Projects
- Solutions (Sparse)
How to Use This Page¶
Each task has a goal, a self-check (how to know you succeeded), and a hint. Try the task before reading the hint, and write the self-check assertion first so you have a target. Sparse solutions for the trickier tasks are at the bottom — consult them only after a genuine attempt. The golden rule running through every task: before you write code, say out loud whether you're dealing with metadata (someone must read it) or behavior (it runs and wraps).
Warm-Up¶
Task W1 — Classify the @¶
For each, state (a) metadata or behavior, (b) who reads/runs it, (c) when: @Override, @app.route("/x") (Flask), @property, @Entity (JPA), [Required] (C#), @Component (Angular), @lru_cache.
- Self-check: You can name the reader and the timing for all seven, and you flagged that
@Overrideis compile-time-only while@Entityis runtime-reflection. - Hint: Apply the deletion test — "if I remove the tool that reads this, does behavior change?"
Task W2 — The rewrite¶
Rewrite @a @b over def f(): ... as explicit assignments, and state the call-time execution order of the wrappers.
- Self-check: You wrote
f = a(b(f))and saida's wrapper executes first (outermost),b's last. - Hint: Decorators apply bottom-up; they execute top-down.
Task W3 — The inert annotation¶
Define a Java annotation @Cool and apply it to a method. Predict the program's behavior.
- Self-check: You predicted no effect whatsoever, and you can explain why (no reader, and it would need
@Retention(RUNTIME)even to be reflectable). - Hint: An annotation is half a feature.
Python Decorator Tasks¶
Task P1 — Timing decorator with wraps¶
Write @timed that prints how long the wrapped function took and returns its result unchanged. Preserve the function's name and docstring.
- Self-check:
timed-decoratedadd.__name__ == "add"andadd.__doc__is preserved; timing prints; the return value is correct. - Hint:
functools.wraps(func)on the innerwrapper; usetime.perf_counter().
Task P2 — Prove wraps matters¶
Write the same decorator without functools.wraps and assert that __name__ is now wrong.
- Self-check:
assert decorated.__name__ == "wrapper"passes withoutwraps, and fails (correctly) once you addwraps. - Hint: Print
decorated.__name__before and after adding@functools.wraps.
Task P3 — Decorator factory @retry(times)¶
Write a parameterized @retry(times=3) that re-invokes the function on exception up to times attempts, re-raising the last exception.
- Self-check: A function that fails twice then succeeds returns its value with
@retry(times=3); a function that always fails raises after exactlytimesattempts (count them). - Hint: Three layers —
retry(times)→decorator(func)→wrapper(*a, **k).
Task P4 — Bare-or-parameterized decorator¶
Make a decorator @trace that works both as @trace and as @trace(prefix=">>").
- Self-check: Both
@traceand@trace(prefix="X")produce working, identity-preserving wrappers. - Hint:
def trace(func=None, *, prefix="")and returnfunctools.partial(trace, prefix=prefix)whenfunc is None.
Task P5 — Class decorator @auto_repr¶
Write a class decorator that adds a __repr__ listing all instance attributes as Name(a=.., b=..).
- Self-check:
repr(Point(1, 2)) == "Point(x=1, y=2)". - Hint: Define a function and assign
cls.__repr__ = ...; iterateself.__dict__.
Task P6 — Registry via decorator¶
Build a @command(name) decorator that registers functions in a module-level COMMANDS dict and a dispatch(name, *args) that calls the registered function.
- Self-check: Decorating two functions populates
COMMANDSat import;dispatch("greet")calls the right one. - Hint: The factory closes over
nameand mutatesCOMMANDSas a side effect at definition time.
Task P7 — Memoization with eviction trap¶
Write a @memoize decorator. Then construct an input that breaks it (unhashable argument) and fix it.
- Self-check: You can articulate that
memoize(f)(([1,2]))raisesTypeError: unhashable typeand you handled it (reject, or convert to a tuple key). - Hint: Cache keys must be hashable; lists aren't.
Java Annotation Tasks¶
Task J1 — Runtime annotation + reader¶
Define @Benchmark with @Retention(RUNTIME) and @Target(METHOD), apply it, then write a method that reflectively finds all @Benchmark methods of a class and "runs" them (just print their names).
- Self-check: Removing
@Retention(RUNTIME)makes your reader find nothing — verify both states. - Hint:
clazz.getDeclaredMethods()+m.isAnnotationPresent(Benchmark.class).
Task J2 — Retention experiment¶
Take J1's annotation and change retention to SOURCE, then CLASS, then RUNTIME. For each, predict whether your reflective reader sees it.
- Self-check: Only
RUNTIMEis visible to reflection;SOURCEandCLASSare not. Confirm by running. - Hint: Reflection can only see
RUNTIME-retained annotations.
Task J3 — @Override safety¶
Write a subclass that intends to override toString but misspells it. Show that adding @Override turns the silent bug into a compile error.
- Self-check: Without
@Overrideit compiles (and silently doesn't override); with@Overrideit fails to compile. - Hint: The compiler reads
@Overrideand verifies a matching supertype method exists.
Task J4 — Annotation with members¶
Extend @Benchmark to have int warmups() default 0 and a String label() default "". Read both values reflectively.
- Self-check:
m.getAnnotation(Benchmark.class).warmups()returns the value you set; defaults apply when omitted. - Hint: Members look like methods in the
@interface; access them on the annotation instance.
Task J5 — Conceptual: design a code-generating processor¶
On paper, design (don't fully implement) an annotation processor for @GenerateBuilder that emits a XxxBuilder class. List the APT pieces you'd use and the round behavior.
- Self-check: You named
AbstractProcessor,getSupportedAnnotationTypes, theFiler, theMessager, theElementmodel,META-INF/servicesregistration, and the round model (generated files compiled in a later round). - Hint: You read the structure (
Elements), not values; you write source via theFiler.
TypeScript / C# Tasks¶
Task T1 — Method-wrapping decorator (legacy TS)¶
With experimentalDecorators: true, write a @log method decorator that logs the method name on each call.
- Self-check: Calling the method prints its name then runs normally.
- Hint: Legacy signature
(target, key, descriptor); wrapdescriptor.value.
Task T2 — DI metadata round-trip¶
With experimentalDecorators and emitDecoratorMetadata on and reflect-metadata imported, read design:paramtypes of a decorated class and print the constructor parameter types.
- Self-check:
Reflect.getMetadata('design:paramtypes', Service)returns the constructor's class types; removing the class decorator makes itundefined. - Hint: The class needs a decorator for metadata to be emitted at all.
Task T3 — Break and explain interface injection¶
Try to inject a dependency typed as an interface and observe the failure. Explain it.
- Self-check: You can state that interfaces erase to
Object, sodesign:paramtypescan't carry them; the fix is an injection token. - Hint: Types are erased; only concrete classes survive as metadata.
Task T4 — C# attribute + reflection¶
Define a [Retry(times)] attribute and read its Times value via reflection on a method.
- Self-check:
GetCustomAttribute<RetryAttribute>().Timesreturns the value passed in brackets. - Hint:
[AttributeUsage]is the@Targetanalogue; constructor args carry the data.
Cross-Cutting & Trap Tasks¶
Task X1 — Stacking order security bug¶
Write @authorize and @cache decorators in Python. Demonstrate that putting @cache outside @authorize lets an unauthorized caller get a cached result.
- Self-check: With the wrong order, a request that should be denied returns a cached value; with
@authorizeoutermost, it's correctly denied. - Hint:
@authorizeover@cachemeansauthorize(cache(f))— authorize runs first.
Task X2 — The parentheses trap¶
Take your @retry(times) factory and apply it as bare @retry (no parens). Capture the error and explain it.
- Self-check: You can explain that the function got passed as
times, so the factory misbehaves; the correct usage needs@retry(3). - Hint: A factory must be called to produce the decorator.
Task X3 — Self-invocation (conceptual / Spring)¶
Explain, in writing, why a @Transactional method called as this.method() from within the same bean doesn't run in a transaction, and give two fixes.
- Self-check: You mention the proxy, that self-calls bypass it, and offer fixes (inject the bean into itself; split beans;
TransactionTemplate). - Hint: The annotation's behavior lives in a proxy, not the object.
Task X4 — Identity loss in the wild¶
Build a tiny router that dispatches on func.__name__. Decorate a handler without functools.wraps and show the router breaks; fix it.
- Self-check: Without
wraps, all handlers register as"wrapper"and collide; withwraps, names are correct. - Hint: This is why
wrapsis non-optional for name-keyed frameworks.
Stretch Projects¶
Task S1 — Mini DI container (Python)¶
Build a container where @injectable marks classes and the container resolves constructor dependencies by reading type annotations (inspect.signature / __annotations__), recursively constructing dependencies.
- Self-check:
container.resolve(Service)returns aServicewith its dependencies auto-constructed; a missing binding raises a clear error. - Hint: Python's
inspect.signature(cls).parametersgives parameter annotations — yourdesign:paramtypesequivalent.
Task S2 — Validation framework (runtime)¶
Build annotations/markers @not_null, @min_len(n) for class fields (use Python field annotations or a small descriptor), and a validate(obj) that reflects over them and returns a list of violations.
- Self-check: An object violating two rules returns exactly two violation messages; a valid object returns none.
- Hint: Store constraints as metadata on the class; reflect over them in
validate.
Task S3 — Build-vs-runtime comparison write-up¶
Implement the same tiny feature (e.g., auto-generated __repr__) two ways: a runtime class decorator and a (sketched) compile-time generator. Write up the cost trade-off.
- Self-check: Your write-up names the trade-off — runtime pays at definition/startup and per introspection; compile-time pays at build and is free afterward.
- Hint: This is the central axis of the whole topic in miniature.
Solutions (Sparse)¶
P3 — @retry(times)¶
import functools
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last = None
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last = e
raise last
return wrapper
return decorator
Key: the factory layer captures times; the decorator layer captures func; the wrapper retries. Count attempts to verify times is honored.
P4 — bare-or-parameterized¶
import functools
def trace(func=None, *, prefix=""):
if func is None:
return functools.partial(trace, prefix=prefix)
@functools.wraps(func)
def wrapper(*a, **k):
print(f"{prefix}{func.__name__}")
return func(*a, **k)
return wrapper
When called with arguments only, func is None, so we return a partial that will receive the function next.
P6 — registry¶
import functools
COMMANDS = {}
def command(name):
def deco(func):
COMMANDS[name] = func # side effect at import/definition time
@functools.wraps(func)
def wrapper(*a, **k):
return func(*a, **k)
return wrapper
return deco
def dispatch(name, *a, **k):
return COMMANDS[name](*a, **k)
Note the registration happens when the module is imported, not when the function is called — the classic framework-router pattern.
J1 — runtime annotation + reader (sketch)¶
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Benchmark { }
static void run(Class<?> c) {
for (Method m : c.getDeclaredMethods())
if (m.isAnnotationPresent(Benchmark.class))
System.out.println("benchmark: " + m.getName());
}
Both halves required: the RUNTIME-retained annotation and the reflective reader. Drop RUNTIME and the reader prints nothing.
X1 — stacking order¶
@authorize # outermost: runs first, can deny before cache is consulted
@cache
def get_secret(user): ...
authorize(cache(get_secret)) — authorization happens before any cache lookup. Reverse the order and cache(authorize(...)) serves cached results to anyone, bypassing the check. Order is a security decision.
S1 — mini DI (sketch)¶
import inspect
REGISTRY = set()
def injectable(cls):
REGISTRY.add(cls)
return cls
def resolve(cls):
sig = inspect.signature(cls.__init__)
args = []
for name, p in sig.parameters.items():
if name == "self":
continue
dep = p.annotation # the "design:paramtypes" equivalent
args.append(resolve(dep))
return cls(*args)
inspect.signature reading parameter annotations is Python's analogue of TypeScript's design:paramtypes — the same idea (read declared types to wire dependencies), without needing a metadata polyfill because Python keeps annotations accessible at runtime.
In this topic
- interview
- tasks