Chapter 17 · The utility layer

17.5 Asking a callable what it accepts: django/utils/inspect.py

django/utils/inspect.py answers what Django asks of a function a project hands it: whether it accepts **kwargs, what its parameters are called, whether a method takes nothing but its instance. Its signature and getfullargspec wrap inspect.signature and inspect.getfullargspec so that, on Python 3.14 and later, an annotation naming an undefined class does not make them fail.

Django calls code it did not write with arguments it fixes itself. Some of its registries ask when handed a function, and refuse one whose parameters the later call could not fill; others ask so as to adapt to an override that lacks a parameter a release has added.

Annotations that cannot be evaluated: signature and getfullargspec

Python 3.14 evaluates a function’s annotations only when they are read, so an annotation may name a class imported only under typing.TYPE_CHECKING; inspect.signature, which evaluates them by default, then raises NameError. Django’s signature passes annotationlib.Format.FORWARDREF: an annotation that resolves is evaluated, and a name that does not is left as an annotationlib.ForwardRef. Below 3.14 it calls inspect.signature as it is. Django does not use the annotations, the docstring says.

# harbour/office/receivers.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from harbour.crew.models import Sailor


def log_arrival(sender, instance, **kwargs): ...


def count_ship(sender, instance): ...


def assign_pilot(ship, pilot: Sailor, *, berth=1, **orders): ...

A run on Python 3.14:

inspect.signature(assign_pilot)                  NameError: name 'Sailor' is not defined
inspect.getfullargspec(assign_pilot)             TypeError: unsupported callable
signature(assign_pilot)                          (ship, pilot: Sailor, *, berth=1, **orders)
the annotation of pilot, its type                <class 'annotationlib.ForwardRef'>
getfullargspec(assign_pilot).args                ['ship', 'pilot']

inspect.getfullargspec on 3.14 takes no format, and calls the private inspect._signature_from_callable with the default one, which evaluates the annotations. The docstring of lazy_annotations names CPython issue 141560 as fixed in 3.15, and on 3.15 the wrapper passes the format to inspect.getfullargspec itself. On 3.14 the wrapper calls it inside lazy_annotations, which swaps inspect._signature_from_callable for safe_signature_from_callable, a functools.partial of it adding the format, and back on the way out, under a module-level threading.Lock.

Template code calls getfullargspec: Library’s tag decorators as a tag is registered, and FilterExpression.args_check, counting a filter’s arguments, as a template is compiled (Libraries: Library, custom tags and {% load %}; Library.simple_tag, FilterExpression.args_check).

The questions, and their cache

Every question but is_module_level_function starts at _get_callable_parameters: a bound method is asked as its __func__ less the first parameter, which its instance fills. The parameters come from _get_func_parameters, under functools.lru_cache of 512 entries keyed by the function and whether the first is dropped, so the bound methods of every instance share one entry; the function read from the class has another. Here endeavour and beagle are the two ships, and the cache, which Django’s own calls have filled by then, is emptied before the methods are asked:

func_accepts_kwargs(log_arrival)                 True
func_accepts_kwargs(count_ship)                  False
get_func_args(assign_pilot)                      ['ship', 'pilot', 'berth']
get_func_full_args(assign_pilot)                 [('ship',), ('pilot',), ('berth', 1), ('**orders',)]
_get_func_parameters.cache_clear()
method_has_no_args(Ship.clean)                   True
method_has_no_args(endeavour.clean)              True
method_has_no_args(beagle.clean)                 True
_get_func_parameters.cache_info()                CacheInfo(hits=1, misses=2, maxsize=512, currsize=2)

func_accepts_kwargs and func_accepts_var_args say whether there is a ** or a * parameter, and get_func_args names the others. get_func_full_args gives each parameter’s name, with * or ** where it gathers, and its default if any, passing over "self". func_supports_parameter asks whether a name can be passed by keyword; **kwargs, which would take it, does not count.

method_has_no_args counts the parameters that are neither * nor **, and wants none for a bound method and one for anything else. A list’s count, a built-in of one argument, passes, as inspect.ismethod is false for a built-in; Paginator.count tests for one first (Pagination: Paginator and Page). is_module_level_function is true of a Python function whose qualified name holds no "<locals>", which a method of a module-level class, read from the class, also passes.

Where Django asks