Annotation Copy¶
Enhanced Example with Explanation¶
You can copy function annotations from one function (source_func) to another function (target_func) using the __annotations__ attribute. However, since target_func has **kwargs, it's a bit different because **kwargs is a catch-all argument for keyword arguments. We can"t directly assign annotations to **kwargs using __annotations__, but we can handle the rest easily.
Here"s an example with more details:
def source_func(a: str, b: str):
pass # Function implementation
def target_func(**kwargs):
pass # Function implementation
# Copy annotations from source_func to target_func
target_func.__annotations__ = source_func.__annotations__
# Print the annotations of target_func after copying
print(target_func.__annotations__) # {'a': <class 'str'>, 'b': <class 'str'>}
# You could also add annotations for **kwargs if you like:
target_func.__annotations__['kwargs'] = dict # Optional: you can specify that kwargs should be a dictionary
# Now print it with the custom annotation for kwargs
print(target_func.__annotations__) # {'a': <class 'str'>, 'b': <class 'str'>, 'kwargs': <class 'dict'>}
Things to Note¶
-
Annotations Copying:
target_func.__annotations__is assigned the dictionary of annotations fromsource_func. This will copy over any parameters and their types, butkwargsdoesn't have a specific type here because it"s a flexible argument. You could add a specific type for**kwargsif you want it to be more explicit (e.g.,dictorMapping). -
Handling
**kwargs: If you wanttarget_functo have a more specific annotation for**kwargs(e.g., specifying that it is a dictionary of key-value pairs), you can manually set this after copying the annotations. This is useful when**kwargsis involved, since you can"t directly annotate\*\*kwargslike regular arguments.
Advanced Use Case¶
If target_func should accept a flexible set of keyword arguments but with specific types (e.g., a: str, b: int, etc., inside **kwargs), you might want to reflect that more clearly in the annotations.
For example:
from typing import Dict
def source_func(a: str, b: str):
pass # Function implementation
def target_func(**kwargs):
pass # Function implementation
# Copy annotations from source_func to target_func
target_func.__annotations__ = source_func.__annotations__
# Add specific typing for **kwargs using a dictionary
target_func.__annotations__['kwargs'] = Dict[str, str] # You can define that kwargs is a dictionary of string keys and values
# Now print target_func's annotations
print(target_func.__annotations__) # {'a': <class 'str'>, 'b': <class 'str'>, 'kwargs': typing.Dict[str, str]}
Summary¶
- Annotations: You can copy annotations from one function to another, which makes code more dynamic and flexible.
**kwargs: Since\*\*kwargsrepresents a flexible set of keyword arguments, you need to handle it explicitly by adding types manually.- Manual Refinement: If
target_funcis supposed to take certain known types in**kwargs, you can specify that by using types likeDictorMappingfrom thetypingmodule.
✅ Best runtime + introspection solution: use inspect.Signature¶
If your goal is:
- reuse the parameter types
- keep
build(**kwargs) - support runtime validation / introspection
👉 Use inspect.signature instead of copying __annotations__.
import inspect
def mask_(a: str, b: str):
pass
def build(**kwargs):
pass
sig = inspect.signature(mask_)
# expose expected kwargs as metadata
build.__signature__ = sig
build.__annotations__ = sig.annotations
Why this is better¶
✔ preserves:
- annotations
- parameter names
- defaults
- keyword-only vs positional info
✔ works with:
help(build)inspect.signature(build)- many frameworks (FastAPI, Typer, Click, Pydantic)
🚫 Still runtime-only (type checkers won"t "see" it)
✅ Best for static typing (mypy / pyright): TypedDict¶
If build(**kwargs) is supposed to accept exactly the same keywords as mask_, this is the cleanest solution.
from typing import TypedDict
class MaskArgs(TypedDict):
a: str
b: str
def mask_(a: str, b: str):
pass
def build(**kwargs: MaskArgs):
pass
Why this is better¶
✔ static type checkers understand it ✔ IDE autocomplete works ✔ no runtime mutation ✔ explicit and readable
This is the recommended approach for modern Python typing.
✅ Best "DRY" solution: derive TypedDict automatically¶
If you really want to avoid repeating types:
import inspect
from typing import TypedDict, get_type_hints
def typed_dict_from_func(func):
return TypedDict(
f"{func.__name__}Args",
get_type_hints(func),
)
def mask_(a: str, b: str):
pass
MaskArgs = typed_dict_from_func(mask_)
def build(**kwargs: MaskArgs):
pass
✔ single source of truth ✔ type-checker friendly ✔ no annotation copying hacks
❌ What"s not ideal (but works)¶
❌ breaks tooling ❌ not seen by type checkers ❌ loses defaults & signature info ❌ mutation at runtime
🔍 Recommendation Summary¶
| Goal | Best Option |
|---|---|
| Runtime introspection | inspect.Signature |
| Static typing / IDE | TypedDict |
| DRY + typing | auto-generated TypedDict |
| Quick hack | copy __annotations__ |