From a92e6a349f62afdffd8f1a1ecbb9866d499fc056 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 10:57:38 +0100 Subject: [PATCH 01/16] Refactor `Dependant.oauth_scopes` property for improved memory usage --- fastapi/dependencies/models.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 25ffb0d2da..c0a7e65524 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -49,15 +49,20 @@ class Dependant: use_cache: bool = True path: str | None = None scope: Literal["function", "request"] | None = None + # Lazy cached fields + _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) - @cached_property + @property def oauth_scopes(self) -> list[str]: - scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] - # This doesn't use a set to preserve order, just in case - for scope in self.own_oauth_scopes or []: - if scope not in scopes: - scopes.append(scope) - return scopes + if self._oauth_scopes_cache is None: + scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] + # This doesn't use a set to preserve order, just in case + for scope in self.own_oauth_scopes or []: + if scope not in scopes: + scopes.append(scope) + self._oauth_scopes_cache = scopes + + return self._oauth_scopes_cache @cached_property def cache_key(self) -> DependencyCacheKey: From cf1a8bb42d147c7b33ac0511fab5c434b7d82f5c Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 10:59:41 +0100 Subject: [PATCH 02/16] Refactor `Dependant.cache_key` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index c0a7e65524..a79ca84351 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -51,6 +51,7 @@ class Dependant: scope: Literal["function", "request"] | None = None # Lazy cached fields _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) + _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -64,16 +65,19 @@ class Dependant: return self._oauth_scopes_cache - @cached_property + @property def cache_key(self) -> DependencyCacheKey: - scopes_for_cache = ( - tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else () - ) - return ( - self.call, - scopes_for_cache, - self.computed_scope or "", - ) + if self._cache_key_cache is None: + scopes_for_cache = ( + tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else () + ) + self._cache_key_cache = ( + self.call, + scopes_for_cache, + self.computed_scope or "", + ) + + return self._cache_key_cache @cached_property def _uses_scopes(self) -> bool: From 1d23ed03b95b72ddfcdd87d3ab51056fcceba85b Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:02:26 +0100 Subject: [PATCH 03/16] Refactor `Dependant._uses_scopes` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index a79ca84351..9bfff02a77 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -52,6 +52,7 @@ class Dependant: # Lazy cached fields _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) + _uses_scopes_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -79,18 +80,25 @@ class Dependant: return self._cache_key_cache - @cached_property + @property def _uses_scopes(self) -> bool: - if self.own_oauth_scopes: - return True - if self.security_scopes_param_name is not None: - return True - if self._is_security_scheme: - return True - for sub_dep in self.dependencies: - if sub_dep._uses_scopes: - return True - return False + if self._uses_scopes_cache is None: + if self.own_oauth_scopes: + self._uses_scopes_cache = True + elif self.security_scopes_param_name is not None: + self._uses_scopes_cache = True + elif self._is_security_scheme: + self._uses_scopes_cache = True + + for sub_dep in self.dependencies: + if sub_dep._uses_scopes: + self._uses_scopes_cache = True + break + + if self._uses_scopes_cache is None: + self._uses_scopes_cache = False + + return self._uses_scopes_cache @cached_property def _is_security_scheme(self) -> bool: From a8f68a382feec98990df1c7d5e6df5cfb53bd27e Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:04:30 +0100 Subject: [PATCH 04/16] Refactor `Dependant._is_security_scheme` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 9bfff02a77..d0791fbce7 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -53,6 +53,7 @@ class Dependant: _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) _uses_scopes_cache: bool = field(default=None, init=False, repr=False) + _is_security_scheme_cache: bool = field(default=False, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -97,15 +98,19 @@ class Dependant: if self._uses_scopes_cache is None: self._uses_scopes_cache = False - + return self._uses_scopes_cache - @cached_property + @property def _is_security_scheme(self) -> bool: - if self.call is None: - return False # pragma: no cover - unwrapped = _unwrapped_call(self.call) - return isinstance(unwrapped, SecurityBase) + if self._is_security_scheme_cache is None: + if self.call is None: + self._is_security_scheme_cache = False # pragma: no cover + else: + unwrapped = _unwrapped_call(self.call) + self._is_security_scheme_cache = isinstance(unwrapped, SecurityBase) + + return self._is_security_scheme_cache # Mainly to get the type of SecurityBase, but it's the same self.call @cached_property From b89e501c7513101eb56d065943369822652b7a85 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:06:52 +0100 Subject: [PATCH 05/16] Refactor `Dependant._security_scheme` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d0791fbce7..d325ceeb1a 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -54,6 +54,7 @@ class Dependant: _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) _uses_scopes_cache: bool = field(default=None, init=False, repr=False) _is_security_scheme_cache: bool = field(default=False, init=False, repr=False) + _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -113,11 +114,14 @@ class Dependant: return self._is_security_scheme_cache # Mainly to get the type of SecurityBase, but it's the same self.call - @cached_property + @property def _security_scheme(self) -> SecurityBase: - unwrapped = _unwrapped_call(self.call) - assert isinstance(unwrapped, SecurityBase) - return unwrapped + if self._security_scheme_cache is None: + unwrapped = _unwrapped_call(self.call) + assert isinstance(unwrapped, SecurityBase) + self._security_scheme_cache = unwrapped + + return self._security_scheme_cache @cached_property def _security_dependencies(self) -> list["Dependant"]: From 381ffe4ecae451af43e5eb6f423381a858a7fe8f Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:07:10 +0100 Subject: [PATCH 06/16] Fix wrong default --- fastapi/dependencies/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d325ceeb1a..7a18d5f515 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -53,7 +53,7 @@ class Dependant: _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) _uses_scopes_cache: bool = field(default=None, init=False, repr=False) - _is_security_scheme_cache: bool = field(default=False, init=False, repr=False) + _is_security_scheme_cache: bool = field(default=None, init=False, repr=False) _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) @property From 6a074fff5b65ab8c1539d4a3111bccdcdc7469e6 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:08:54 +0100 Subject: [PATCH 07/16] Refactor `Dependant._security_dependencies` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 7a18d5f515..aeda28ac15 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -55,6 +55,7 @@ class Dependant: _uses_scopes_cache: bool = field(default=None, init=False, repr=False) _is_security_scheme_cache: bool = field(default=None, init=False, repr=False) _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) + _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -123,10 +124,13 @@ class Dependant: return self._security_scheme_cache - @cached_property + @property def _security_dependencies(self) -> list["Dependant"]: - security_deps = [dep for dep in self.dependencies if dep._is_security_scheme] - return security_deps + if self._security_dependencies_cache is None: + security_deps = [dep for dep in self.dependencies if dep._is_security_scheme] + self._security_dependencies_cache = security_deps + + return self._security_dependencies_cache @cached_property def is_gen_callable(self) -> bool: From 7c8bf4d688788179b07442e8dd1f1a7c173596d6 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:13:10 +0100 Subject: [PATCH 08/16] Refactor `Dependant.is_gen_callable` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 63 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index aeda28ac15..c2737df23a 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -56,6 +56,7 @@ class Dependant: _is_security_scheme_cache: bool = field(default=None, init=False, repr=False) _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) + _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -132,31 +133,45 @@ class Dependant: return self._security_dependencies_cache - @cached_property + @property def is_gen_callable(self) -> bool: - if self.call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(self.call) - ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): - return True - if inspect.isclass(_unwrapped_call(self.call)): - return False - dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 - if dunder_call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(dunder_call) - ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): - return True - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 - if dunder_unwrapped_call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(dunder_unwrapped_call) - ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): - return True - return False + if self._is_gen_callable_cache is None: + if self.call is None: + self._is_gen_callable_cache = False # pragma: no cover + elif inspect.isgeneratorfunction( + _impartial(self.call) + ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): + self._is_gen_callable_cache = True + elif inspect.isclass(_unwrapped_call(self.call)): + self._is_gen_callable_cache = False + + # optimization to exit early + if self._is_gen_callable_cache is not None: + return self._is_gen_callable_cache + + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + self._is_gen_callable_cache = False # pragma: no cover + elif inspect.isgeneratorfunction( + _impartial(dunder_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): + self._is_gen_callable_cache = True + + # optimization to exit early + if self._is_gen_callable_cache is not None: + return self._is_gen_callable_cache + + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + self._is_gen_callable_cache = False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): + self._is_gen_callable_cache = True + else: + self._is_gen_callable_cache = False + + return self._is_gen_callable_cache @cached_property def is_async_gen_callable(self) -> bool: From dd109d369587eb91f1a2788fe6d430ccbc82e321 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:15:40 +0100 Subject: [PATCH 09/16] Refactor `Dependant.is_async_gen_callable` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 65 ++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index c2737df23a..a19afd2fa8 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -57,6 +57,7 @@ class Dependant: _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) + _is_async_gen_callable_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -145,7 +146,6 @@ class Dependant: elif inspect.isclass(_unwrapped_call(self.call)): self._is_gen_callable_cache = False - # optimization to exit early if self._is_gen_callable_cache is not None: return self._is_gen_callable_cache @@ -157,7 +157,6 @@ class Dependant: ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): self._is_gen_callable_cache = True - # optimization to exit early if self._is_gen_callable_cache is not None: return self._is_gen_callable_cache @@ -170,34 +169,46 @@ class Dependant: self._is_gen_callable_cache = True else: self._is_gen_callable_cache = False - + return self._is_gen_callable_cache - @cached_property + @property def is_async_gen_callable(self) -> bool: - if self.call is None: - return False # pragma: no cover - if inspect.isasyncgenfunction( - _impartial(self.call) - ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): - return True - if inspect.isclass(_unwrapped_call(self.call)): - return False - dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 - if dunder_call is None: - return False # pragma: no cover - if inspect.isasyncgenfunction( - _impartial(dunder_call) - ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)): - return True - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 - if dunder_unwrapped_call is None: - return False # pragma: no cover - if inspect.isasyncgenfunction( - _impartial(dunder_unwrapped_call) - ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): - return True - return False + if self._is_async_gen_callable_cache is None: + if self.call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(self.call) + ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): + self._is_async_gen_callable_cache = True + elif inspect.isclass(_unwrapped_call(self.call)): + self._is_async_gen_callable_cache = False + + if self._is_async_gen_callable_cache is not None: + return self._is_async_gen_callable_cache + + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(dunder_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)): + self._is_async_gen_callable_cache = True + + if self._is_async_gen_callable_cache is not None: + return self._is_async_gen_callable_cache + + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): + self._is_async_gen_callable_cache = True + else: + self._is_async_gen_callable_cache = False + + return self._is_async_gen_callable_cache @cached_property def is_coroutine_callable(self) -> bool: From a69af67db34b25f7cc1546902f7ce6acc77977bf Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:18:15 +0100 Subject: [PATCH 10/16] Refactor `Dependant.is_coroutine_callable` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 69 ++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index a19afd2fa8..f1aace7329 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -58,6 +58,7 @@ class Dependant: _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) _is_async_gen_callable_cache: bool = field(default=None, init=False, repr=False) + _is_coroutine_callable_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -210,35 +211,47 @@ class Dependant: return self._is_async_gen_callable_cache - @cached_property + @property def is_coroutine_callable(self) -> bool: - if self.call is None: - return False # pragma: no cover - if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction( - _impartial(self.call) - ): - return True - if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction( - _unwrapped_call(self.call) - ): - return True - if inspect.isclass(_unwrapped_call(self.call)): - return False - dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 - if dunder_call is None: - return False # pragma: no cover - if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction( - _unwrapped_call(dunder_call) - ): - return True - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 - if dunder_unwrapped_call is None: - return False # pragma: no cover - if iscoroutinefunction( - _impartial(dunder_unwrapped_call) - ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)): - return True - return False + if self._is_coroutine_callable_cache is None: + if self.call is None: + self._is_coroutine_callable_cache = False # pragma: no cover + elif inspect.isroutine(_impartial(self.call)) and iscoroutinefunction( + _impartial(self.call) + ): + self._is_coroutine_callable_cache = True + elif inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction( + _unwrapped_call(self.call) + ): + self._is_coroutine_callable_cache = True + elif inspect.isclass(_unwrapped_call(self.call)): + self._is_coroutine_callable_cache = False + + if self._is_coroutine_callable_cache is not None: + return self._is_coroutine_callable_cache + + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + self._is_coroutine_callable_cache = False # pragma: no cover + elif iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction( + _unwrapped_call(dunder_call) + ): + self._is_coroutine_callable_cache = True + + if self._is_coroutine_callable_cache is not None: + return self._is_coroutine_callable_cache + + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + self._is_coroutine_callable_cache = False # pragma: no cover + elif iscoroutinefunction( + _impartial(dunder_unwrapped_call) + ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)): + self._is_coroutine_callable_cache = True + else: + self._is_coroutine_callable_cache = False + + return self._is_coroutine_callable_cache @cached_property def computed_scope(self) -> str | None: From da148fb8a364c17112863223f8b365094ef00eea Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:20:05 +0100 Subject: [PATCH 11/16] Refactor `Dependant.computed_scope` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index f1aace7329..b7dac3f29d 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -59,6 +59,7 @@ class Dependant: _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) _is_async_gen_callable_cache: bool = field(default=None, init=False, repr=False) _is_coroutine_callable_cache: bool = field(default=None, init=False, repr=False) + _computed_scope_cache: str | None = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -250,13 +251,17 @@ class Dependant: self._is_coroutine_callable_cache = True else: self._is_coroutine_callable_cache = False - + return self._is_coroutine_callable_cache - @cached_property + @property def computed_scope(self) -> str | None: - if self.scope: - return self.scope - if self.is_gen_callable or self.is_async_gen_callable: - return "request" - return None + if self._computed_scope_cache is None: + if self.scope: + self._computed_scope_cache = self.scope + elif self.is_gen_callable or self.is_async_gen_callable: + self._computed_scope_cache = "request" + else: + self._computed_scope_cache = None + + return self._computed_scope_cache From 2daccb0e32c67afe12ac265012b2ff089e05773a Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:20:49 +0100 Subject: [PATCH 12/16] Enable `slots` option ``` Samples: 200000 - current=156.8 MB peak=156.8 MB Samples: 100000 - current=78.4 MB peak=78.4 MB Samples: 50000 - current=39.2 MB peak=39.2 MB Samples: 1000 - current=0.8 MB peak=0.8 MB Samples: 500 - current=0.4 MB peak=0.4 MB ``` --- fastapi/dependencies/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index b7dac3f29d..85185e067c 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -28,7 +28,7 @@ def _impartial(func: Callable[..., Any]) -> Callable[..., Any]: return func -@dataclass +@dataclass(slots=True) class Dependant: path_params: list[ModelField] = field(default_factory=list) query_params: list[ModelField] = field(default_factory=list) From f44a2329ec5ef644876db80cd57a4915fb572f09 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:22:37 +0100 Subject: [PATCH 13/16] Make cache field typing more accurate --- fastapi/dependencies/models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 85185e067c..81cce52944 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -50,15 +50,15 @@ class Dependant: path: str | None = None scope: Literal["function", "request"] | None = None # Lazy cached fields - _oauth_scopes_cache: list[str] = field(default=None, init=False, repr=False) - _cache_key_cache: DependencyCacheKey = field(default=None, init=False, repr=False) - _uses_scopes_cache: bool = field(default=None, init=False, repr=False) - _is_security_scheme_cache: bool = field(default=None, init=False, repr=False) - _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) - _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) - _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) - _is_async_gen_callable_cache: bool = field(default=None, init=False, repr=False) - _is_coroutine_callable_cache: bool = field(default=None, init=False, repr=False) + _oauth_scopes_cache: list[str] | None = field(default=None, init=False, repr=False) + _cache_key_cache: DependencyCacheKey | None = field(default=None, init=False, repr=False) + _uses_scopes_cache: bool | None = field(default=None, init=False, repr=False) + _is_security_scheme_cache: bool | None = field(default=None, init=False, repr=False) + _security_scheme_cache: SecurityBase | None = field(default=None, init=False, repr=False) + _security_dependencies_cache: list["Dependant"] | None = field(default=None, init=False, repr=False) + _is_gen_callable_cache: bool | None = field(default=None, init=False, repr=False) + _is_async_gen_callable_cache: bool | None = field(default=None, init=False, repr=False) + _is_coroutine_callable_cache: bool | None = field(default=None, init=False, repr=False) _computed_scope_cache: str | None = field(default=None, init=False, repr=False) @property From b4339667e234378b2f8462b060ade2353ffbca48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:46:19 +0000 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/models.py | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 81cce52944..2fbc96d92c 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -2,7 +2,7 @@ import inspect import sys from collections.abc import Callable from dataclasses import dataclass, field -from functools import cached_property, partial +from functools import partial from typing import Any, Literal from fastapi._compat import ModelField @@ -51,14 +51,24 @@ class Dependant: scope: Literal["function", "request"] | None = None # Lazy cached fields _oauth_scopes_cache: list[str] | None = field(default=None, init=False, repr=False) - _cache_key_cache: DependencyCacheKey | None = field(default=None, init=False, repr=False) + _cache_key_cache: DependencyCacheKey | None = field( + default=None, init=False, repr=False + ) _uses_scopes_cache: bool | None = field(default=None, init=False, repr=False) _is_security_scheme_cache: bool | None = field(default=None, init=False, repr=False) - _security_scheme_cache: SecurityBase | None = field(default=None, init=False, repr=False) - _security_dependencies_cache: list["Dependant"] | None = field(default=None, init=False, repr=False) + _security_scheme_cache: SecurityBase | None = field( + default=None, init=False, repr=False + ) + _security_dependencies_cache: list["Dependant"] | None = field( + default=None, init=False, repr=False + ) _is_gen_callable_cache: bool | None = field(default=None, init=False, repr=False) - _is_async_gen_callable_cache: bool | None = field(default=None, init=False, repr=False) - _is_coroutine_callable_cache: bool | None = field(default=None, init=False, repr=False) + _is_async_gen_callable_cache: bool | None = field( + default=None, init=False, repr=False + ) + _is_coroutine_callable_cache: bool | None = field( + default=None, init=False, repr=False + ) _computed_scope_cache: str | None = field(default=None, init=False, repr=False) @property @@ -131,7 +141,9 @@ class Dependant: @property def _security_dependencies(self) -> list["Dependant"]: if self._security_dependencies_cache is None: - security_deps = [dep for dep in self.dependencies if dep._is_security_scheme] + security_deps = [ + dep for dep in self.dependencies if dep._is_security_scheme + ] self._security_dependencies_cache = security_deps return self._security_dependencies_cache @@ -162,7 +174,9 @@ class Dependant: if self._is_gen_callable_cache is not None: return self._is_gen_callable_cache - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + dunder_unwrapped_call = getattr( + _unwrapped_call(self.call), "__call__", None + ) # noqa: B004 if dunder_unwrapped_call is None: self._is_gen_callable_cache = False # pragma: no cover if inspect.isgeneratorfunction( @@ -200,7 +214,9 @@ class Dependant: if self._is_async_gen_callable_cache is not None: return self._is_async_gen_callable_cache - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + dunder_unwrapped_call = getattr( + _unwrapped_call(self.call), "__call__", None + ) # noqa: B004 if dunder_unwrapped_call is None: self._is_async_gen_callable_cache = False # pragma: no cover elif inspect.isasyncgenfunction( @@ -242,7 +258,9 @@ class Dependant: if self._is_coroutine_callable_cache is not None: return self._is_coroutine_callable_cache - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + dunder_unwrapped_call = getattr( + _unwrapped_call(self.call), "__call__", None + ) # noqa: B004 if dunder_unwrapped_call is None: self._is_coroutine_callable_cache = False # pragma: no cover elif iscoroutinefunction( From 17fd86be5774e78715254ad77a1df412c84b2a94 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 12:07:43 +0100 Subject: [PATCH 15/16] Fix ruff linting issue --- fastapi/dependencies/models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 2fbc96d92c..3cf73449b4 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -174,9 +174,11 @@ class Dependant: if self._is_gen_callable_cache is not None: return self._is_gen_callable_cache - dunder_unwrapped_call = getattr( - _unwrapped_call(self.call), "__call__", None - ) # noqa: B004 + dunder_unwrapped_call = getattr( # noqa: B004 + _unwrapped_call(self.call), + "__call__", + None, + ) if dunder_unwrapped_call is None: self._is_gen_callable_cache = False # pragma: no cover if inspect.isgeneratorfunction( @@ -214,9 +216,9 @@ class Dependant: if self._is_async_gen_callable_cache is not None: return self._is_async_gen_callable_cache - dunder_unwrapped_call = getattr( + dunder_unwrapped_call = getattr( # noqa: B004 _unwrapped_call(self.call), "__call__", None - ) # noqa: B004 + ) if dunder_unwrapped_call is None: self._is_async_gen_callable_cache = False # pragma: no cover elif inspect.isasyncgenfunction( @@ -258,9 +260,9 @@ class Dependant: if self._is_coroutine_callable_cache is not None: return self._is_coroutine_callable_cache - dunder_unwrapped_call = getattr( + dunder_unwrapped_call = getattr( # noqa: B004 _unwrapped_call(self.call), "__call__", None - ) # noqa: B004 + ) if dunder_unwrapped_call is None: self._is_coroutine_callable_cache = False # pragma: no cover elif iscoroutinefunction( From 95cd0164cdd986e889fc7e25c1f4a60167e9c4e0 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 12:07:58 +0100 Subject: [PATCH 16/16] Fix mypy linting issue --- fastapi/dependencies/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 3cf73449b4..3744fc7a12 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -3,7 +3,7 @@ import sys from collections.abc import Callable from dataclasses import dataclass, field from functools import partial -from typing import Any, Literal +from typing import Any, Literal, cast from fastapi._compat import ModelField from fastapi.security.base import SecurityBase @@ -18,11 +18,11 @@ else: # pragma: no cover def _unwrapped_call(call: Callable[..., Any] | None) -> Any: if call is None: return call # pragma: no cover - unwrapped = inspect.unwrap(_impartial(call)) + unwrapped = inspect.unwrap(cast(Callable[..., Any], _impartial(call))) return unwrapped -def _impartial(func: Callable[..., Any]) -> Callable[..., Any]: +def _impartial(func: Callable[..., Any] | None) -> Callable[..., Any] | None: while isinstance(func, partial): func = func.func return func