Chapter 17 · The utility layer
17.5 Asking a callable what it accepts: django/utils/inspect.py
django/ 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 and inspect 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; inspect, which evaluates them by default, then raises NameError. Django’s signature passes annotationlib: an annotation that resolves is evaluated, and a name that does not is left as an annotationlib. Below 3.14 it calls inspect 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 on 3.14 takes no format, and calls the private inspect 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 itself. On 3.14 the wrapper calls it inside lazy_annotations, which swaps inspect for safe_signature_from_callable, a functools 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, counting a filter’s arguments, as a template is compiled (Libraries: Library, custom tags and {% load %}; Library, FilterExpression).
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 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
Signal.connectrefuses a receiver without**kwargswhensettings.DEBUGis true (Signals:Signal, receivers and sending;Signal.connect), andCheckRegistrysuch a check whatever the setting (The system checks;.register CheckRegistry)..register BaseTaskBackendrefuses a task whose function is not module-level, or takes a context but not as its first argument (Background tasks:.validate_task Task,TaskResultand the task backends;BaseTaskBackend)..validate_task OperationWriternames an operation’s positional arguments by.serialize get_func_argsof its__init__(Writing a migration file:MigrationWriter;OperationWriter)..serialize _get_from_dbwarns where an override ofModel.from_dbtakes neither fetch_mode nor**kwargs(Deferred fields and fetch modes in a queryset;_get_from_db); the admin warns where an override ofModelAdminlacks action_location (Actions:.get_actions response_actionanddelete_selected;ModelAdmin).._get_actions_with_action_location - admindocs shows as a field a model’s method that takes no argument,
*or**, and lists others withget_func_full_args.