Skip to content

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

  1. Annotations Copying: target_func.__annotations__ is assigned the dictionary of annotations from source_func. This will copy over any parameters and their types, but kwargs doesn't have a specific type here because it"s a flexible argument. You could add a specific type for **kwargs if you want it to be more explicit (e.g., dict or Mapping).

  2. Handling **kwargs: If you want target_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\*\*kwargs like 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 \*\*kwargs represents a flexible set of keyword arguments, you need to handle it explicitly by adding types manually.
  • Manual Refinement: If target_func is supposed to take certain known types in **kwargs, you can specify that by using types like Dict or Mapping from the typing module.

✅ 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)

build.__annotations__ = mask_.__annotations__

❌ 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__