You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RFC: Adding inline type annotations to Requests #7271
Before reporting new issues, be sure to check this comment to see if your problem is already being addressed.
Motivation
Requests has notably been without inline type annotations since type hints entered the Python ecosystem. We've had aspirations of bringing Requests up to par with community standards for a few years but have been unable to properly express the typing contract. This change doesn't solve all of those gaps, but does make progress towards that goal.
The maintainers of typeshed have provided an incredibly useful alternative for Requests typing for quite a while. There are some limitations of type stubs that can't fully express the Requests API though. Cases like iter_content having different return typing based on input, or PreparedRequests needing to be state checked can't be easily expressed. Inline annotations let us own the type contract alongside the code and should help drive better development decisions in the future.
For most use cases, the public API should be accurately typed. There are some internals that are currently suppressed because they need discussion, but the package passes pyright strict mode without errors. We've flagged where the sharp edges are and can refine those over time.
The main goal is to start this migration with community input to make sure we're covering all usage. The "magic" of Requests is that it's very flexible, which also means people use it in creative ways the maintainers don't always anticipate. We want to identify those edge cases early in this process before the types ship in a release.
What's changed
Every class, function, method, and module-level variable in src/requests/ is annotated
No public API changes
Passes typechecking with pyright strict mode
New internal _types.py module for shared type aliases and Protocols
from __future__ import annotations applied everywhere
There are some minor bug fixes included, but they address legitimate defects in Requests that resulted in unhandled runtime exceptions.
Design philosophy
Types should describe the code, not change behavior. This was the primary goal in this process, not adding new constraints.
Where typing forced a choice between runtime strictness and backward compatibility, we chose backward compatibility. That's the lens I'd view this through when things may differ from expectations.
Run pyright or mypy against your code that uses Requests
Try your usual Requests patterns and see if anything doesn't typecheck
What feedback we're looking for
Important
We want feedback from people who actively maintain projects that depend on Requests or use it heavily. Please share your real experience testing this against your code.
Comments that are clearly AI-generated will be hidden or removed. This is a library written "for Humans". The conversation is between maintainers and users, not a prompt.
If you do find issues, we want to hear about it. Specifically:
Compatibility: Does this break anything in your project? Especially if you're doing something unusual with Requests internals.
Type accuracy: Do the types match how you actually use the API? Are return types too narrow or too wide?
Missing coverage: Is there a pattern you use that we're not covering?
We're not looking for style opinions on the annotations themselves or nitpicks on suppression comments. Those are tracked on the PR.
Known limitations
str | bytes URL handling: Requests has historically accepted both str and bytes for URLs. The typing currently declares str only. This is a known gap we're still working through.
urllib3 stub gaps: Some type: ignore comments exist because urllib3's type stubs don't fully cover all the methods we use.
Optional dependency typing: chardet/charset_normalizer detection is imperfect under strict mode since these are optional imports.
RequestsCookieJar and MutableMapping: The RequestsCookieJar's conformance to MutableMapping has some known mismatches (extended method signatures, __iter__ yielding Cookie objects instead of strings) that date back to when the class was first written against the Python 2.6 ABC contract. The current types don't fix that, but should be clearer about what the class does. Future work may be able to refine this further.
Thanks for the efforts of adding inline type annotations.
Here are some issues I've encountered after installing requests from its inline_types_rfc git branch and removing types-requests from my Python environment while using both mypy and ty in my project. I'm including suff that's already listed in the known issues above.
Response.status_code is annotated as int | None, whereas typeshed simply annotates it as int. With None included in a type union, this introduces several typing issues in trivial code snippets:
error[unsupported-operator] Operator >= is not supported between objects of type int | None and Literal[400]
Does it make sense annotating it as int | None just because it's initialized as None?
Session.request() and the various HTTP-verb methods (get, post, head, options, delete, put, patch) all differ from typeshed's definitions. Since my project extends Session.request() and the various methods, this causes compatibility issues, but I hadn't had a deeper look at the exact changes yet
Session.headers is not a MutableMapping (unlike typeshed), leading to typing issues when trying to set the attribute directly (e.g. session.headers = {})
Thanks for taking the time to look through the types, @bastimeyer!
Response.status_code is annotated as int | None, whereas typeshed simply annotates it as int. With None included in a type union, this introduces several typing issues in trivial code snippets:
That's a fair callout. Requests has this issue with Request, PreparedRequest, and Response. You'll see _ValidatedRequest as an attempt to model the state of a PreparedRequest that was actually initialized by prepare() instead of manually. The Response case has this too, but it looks like that wasn't encapsulated as well as it could. A bare Response() will be None, and mocking tooling does that. Whether that's intended usage, is another question. We may need another TypeGuard to constrain a narrow version of Response returned from the APIs, or take the approach Typeshed did and suppress the errors on a bare Response.
Session.request() and the various HTTP-verb methods (get, post, head, options, delete, put, patch) all differ from typeshed's definitions.
I'm assuming this is typeshed's practice of materializing all kwargs directly on every function signature? There's been some discussion about that already here. Once we solve the Unpack layering, I think this will go away. Let me know if I'm missing something though.
Session.headers is not a MutableMapping (unlike typeshed), leading to typing issues when trying to set the attribute directly (e.g. session.headers = {})
This one is interesting but not surprising. I originally had MutableMapping here but had to constrain it to CaseInsensitiveDict. That's both what it's always been documented as being, and a runtime requirement for multiple code paths. You'll end up with inaccurate comparisons (and potentially incorrect headers) if you're using a bare dict. I think we want to keep this one so we're not subtly misleading users. It looks like someone got (reasonably) confused, and changed this incorrectly in typeshed.
Response.iter_lines str/bytes overload issue:
This one is actually correct. Your response with decode_unicode=True is not guaranteed to be Iterable[str]. If Requests cannot determine the encoding, it falls back to bytes. Typing it strictly as Iterably[str] is not true at runtime and is a vector for breakages if your code assumes that.
requests.compat.chardet typing issues: we have a compatibility import for charset_normalizer, based on what requests imports, like this:
This one is also subtle. If neither charset_normalizer or chardet are installed at runtime, you will get back None from your import. I don't think you can actually hit your ImportError fallback in this case? There isn't a released version of Requests that runs on Python 3.9+ that doesn't have the import available. That said, I don't think Requests can actually run if neither is present, so I think this just becomes a runtime error in most cases. I'm open to constraining it to module type, it's just going to be slightly incorrect. Requests does round trip successfully without either character detection library so this is a possibility, although I'd consider it obscure/not generally supported.
I've moved the PR from Draft to Ready for Review. While there may be minor refactors in the coming week or two, I think the changes are largely done. It's safe to assume time for comment will be closing in the coming weeks, please take a look now if you intend to do so.
The class-level attribute annotations on Response are causing issues for downstream projects that subclass it as a dataclass or similar. Other classes in models.py surely have the same problem. It does seem that enabling forward references is the culprit downstream, so I wonder if this something that should be addressed here.
For example, in requests-cache:
@define(auto_attribs=False, repr=False, slots=False)classBaseResponse(Response):
"""Wrapper class for responses returned by :py:class:`.CachedSession`. This mainly exists to provide type hints for extra cache-related attributes that are added to non-cached responses. """
Thanks for the repro, @edgarrmondragon. This is an unfortunately messy side-effect I was not aware of. Presumably this would apply to most dataclass style libraries beyond attrs?
I don't have a strong reason why these were added at class-level other than consistency. Not all attributes can be moved to __init__. Some like Response.connection or RequestsCookieJar._policy aren't actually set in the __init__ (or don't have an explicit __init__). I tried in this commit but these two top-level attributes are stuck where they are.
FWIW moving the annotations to the attribute assignments inside init seems to fix the problem.
Given the above, I'm curious how you aren't hitting errors for HTTPAdapter with connection. Do you have an example of what you tried?
I've noticed some issues with the current definition of ParamsType. I haven't followed this thread since my last post after which I fixed all the issues I ran into, so I'm not sure if this has been brought to attention yet.
ParamsType currently defines Mapping[_ParamsMappingKeyType, _ParamsMappingValueType] as one of the union items, which is a problem, because the key type is invariant in Mapping.
The first example works, while the second one results in typing issues (checked via ty/mypy):
$ ty check foo.py
error[invalid-argument-type]: Argument to function `get` is incorrect
--> foo.py:3:34
|
3 | requests.get("http://localhost", params=params)
| ^^^^^^^^^^^^^ Expected `Mapping[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None] | Iterable[tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]] | str | bytes | None`, found `dict[str, str]`
|
info: type `dict[str, str]` is not assignable to any element of the union `Mapping[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None] | Iterable[tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]] | str | bytes | None`
info: ├── element `bytes` of union `str | bytes | int | float` is not assignable to `str`
info: ├── type `dict[str, str]` is not assignable to protocol `Iterable[tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]]`
info: │ └── protocol member `__iter__` is incompatible
info: │ └── incompatible return types: `Iterator[str]` is not assignable to `Iterator[tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]]`
info: │ └── protocol `Iterator[str]` is not assignable to protocol `Iterator[tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]]`
info: │ └── protocol member `__next__` is incompatible
info: │ └── incompatible return types: `str` is not assignable to `tuple[str | bytes | int | float, Iterable[str | bytes | int | float] | float | int | None]`
info: └── ... omitted 3 union elements without additional context
Typeshed defines it with a SupportsItems protocol, similar to yours:
Great find, @bastimeyer! I've put up #7426 which should match more closely to what typeshed is doing and it brings the API closer to our Iterable inputs. Let me know if you think anything else is missing, otherwise we'll plan to merge that change.
The SupportsItems Protocol changes in #7426 seem fine. At least they fix my issue when installing from that PR branch. Thanks.
But what I just noticed is, why is there a dedicated GetKwargs that's excluding the params keyword from GET requests? GET requests should also allow params, so query strings can be built from a mapping. RequestKwargs includes it, but it's explicitly not used for get().
Also a good find, there's actually asymmetry on the Session.get() API vs the requests.get() API. The kwarg types were built on api.py where params is explicit and then ported over to session.py which is missing the same parameter. I'll get that cleaned up.
Adding a quick comment here to demarcate pre and post release of 2.34.0. Anyone arriving here after this comment, please provide a succinct description of what your input is, the affected APIs, and what you think typing should be. We'll review those on a case-by-case basis.
With Response.reason being explicitly initialized as None and typed accordingly as union of str | None this breaks type checkers that infer their types mainly from the source like pyright. This happens in our code base where we have custom exceptions we raise and that get initialized with the reason as a str.
Interestingly with types-requests installed mypy, ty, and pyright are fine with us simply passing response.reasoninto our custom exception. Onlypyright` complains about
error: Argument of type "str | None" cannot be assigned to parameter "response_reason" of type "str" in function "__init__"
Are you planning to address this or should we handle this?
I also noticed that in the Response.raise_for_status method there's no check for None such that the reason variable in the http_error_msg could be printed as empty.
Thanks for the report, @gothicVI. Reason is inherently optional (even at the HTTP Protocol level) and as you noted initialized as None. That said, we've smoothed this out for other Response attributes like status_code where it's almost never None. Let me think about this one for a bit today and we'll make a decision on if we clean this up in 2.34.1. Initial testing with urllib3 is showing they are normalizing missing values to '' which is the primary path Requests users are going through.
Closing this out since we haven't had any notable feedback in the last few weeks. Anything new can be raised as its own issue. Thanks again everyone who gave feedback and help with reviewing!
Reacted by Seth Larson, Edgar Ramírez Mondragón and Richard Si
Activity
bastimeyer commentedon Mar 25, 2026
Thanks for the efforts of adding inline type annotations.
Here are some issues I've encountered after installing
requestsfrom itsinline_types_rfcgit branch and removingtypes-requestsfrom my Python environment while using both mypy and ty in my project. I'm including suff that's already listed in the known issues above.Response.status_codeis annotated asint | None, whereas typeshed simply annotates it asint. WithNoneincluded in a type union, this introduces several typing issues in trivial code snippets:Does it make sense annotating it as
int | Nonejust because it's initialized asNone?Session.request()and the various HTTP-verb methods (get, post, head, options, delete, put, patch) all differ from typeshed's definitions. Since my project extendsSession.request()and the various methods, this causes compatibility issues, but I hadn't had a deeper look at the exact changes yetrequests/src/requests/sessions.py
Lines 562 to 580 in 781caba
Session.headersis not aMutableMapping(unlike typeshed), leading to typing issues when trying to set the attribute directly (e.g.session.headers = {})Response.iter_linesstr/bytes overload issue:requests/src/requests/models.py
Lines 959 to 979 in 781caba
requests.compat.chardettyping issues: we have a compatibility import for charset_normalizer, based on what requests imports, like this:This now results in ty emitting this error:
nateprewitt commentedon Mar 25, 2026
Thanks for taking the time to look through the types, @bastimeyer!
That's a fair callout. Requests has this issue with
Request,PreparedRequest, andResponse. You'll see _ValidatedRequest as an attempt to model the state of a PreparedRequest that was actually initialized byprepare()instead of manually. The Response case has this too, but it looks like that wasn't encapsulated as well as it could. A bareResponse()will beNone, and mocking tooling does that. Whether that's intended usage, is another question. We may need another TypeGuard to constrain a narrow version of Response returned from the APIs, or take the approach Typeshed did and suppress the errors on a bare Response.I'm assuming this is typeshed's practice of materializing all kwargs directly on every function signature? There's been some discussion about that already here. Once we solve the Unpack layering, I think this will go away. Let me know if I'm missing something though.
This one is interesting but not surprising. I originally had
MutableMappinghere but had to constrain it toCaseInsensitiveDict. That's both what it's always been documented as being, and a runtime requirement for multiple code paths. You'll end up with inaccurate comparisons (and potentially incorrect headers) if you're using a bare dict. I think we want to keep this one so we're not subtly misleading users. It looks like someone got (reasonably) confused, and changed this incorrectly in typeshed.This one is actually correct. Your response with
decode_unicode=Trueis not guaranteed to be Iterable[str]. If Requests cannot determine the encoding, it falls back to bytes. Typing it strictly as Iterably[str] is not true at runtime and is a vector for breakages if your code assumes that.This one is also subtle. If neither
charset_normalizerorchardetare installed at runtime, you will get backNonefrom your import. I don't think you can actually hit your ImportError fallback in this case? There isn't a released version of Requests that runs on Python 3.9+ that doesn't have the import available.That said, I don't think Requests can actually run if neither is present, so I think this just becomes a runtime error in most cases. I'm open to constraining it to module type, it's just going to be slightly incorrect.Requests does round trip successfully without either character detection library so this is a possibility, although I'd consider it obscure/not generally supported.build(security): Update dependency requests to v2.33.0 [SECURITY] (#213)
chore(security): Update dependency requests to v2.33.0 [SECURITY] (#330)
feat(deps): Update dependency requests to v2.33.0 [SECURITY] (#427)
feat(deps): Update dependency requests to v2.33.0 [SECURITY] (#436)
nateprewitt commentedon Apr 29, 2026
I've moved the PR from Draft to Ready for Review. While there may be minor refactors in the coming week or two, I think the changes are largely done. It's safe to assume time for comment will be closing in the coming weeks, please take a look now if you intend to do so.
nateprewitt commentedon May 3, 2026
A pre-release (2.34.0.dev1) is now available on PyPI for easier install.
edgarrmondragon commentedon May 7, 2026
Hey Nate, thanks for publishing that pre-release!
The class-level attribute annotations on
Responseare causing issues for downstream projects that subclass it as a dataclass or similar. Other classes inmodels.pysurely have the same problem. It does seem that enabling forward references is the culprit downstream, so I wonder if this something that should be addressed here.For example, in
requests-cache:source: https://github.com/requests-cache/requests-cache/blob/5ca431fefa89001e09fd22da642867248aea2a01/requests_cache/models/response.py#L28-L32
FWIW moving the annotations to the attribute assignments inside
__init__seems to fix the problem.Here's a MRE
Traceback
uv run --script main.pynateprewitt commentedon May 8, 2026
Thanks for the repro, @edgarrmondragon. This is an unfortunately messy side-effect I was not aware of. Presumably this would apply to most dataclass style libraries beyond
attrs?I don't have a strong reason why these were added at class-level other than consistency. Not all attributes can be moved to
__init__. Some likeResponse.connectionorRequestsCookieJar._policyaren't actually set in the__init__(or don't have an explicit__init__). I tried in this commit but these two top-level attributes are stuck where they are.Given the above, I'm curious how you aren't hitting errors for
HTTPAdapterwithconnection. Do you have an example of what you tried?edgarrmondragon commentedon May 8, 2026
If I move
RequestsCookieJaroutside of theTYPE_CHECKINGblock, then I do get a similar error forHTTPAdapterI gave it a try locally and it seems you can still annotate it within
__init__:I created a PR with the small diff I tried out locally, which fixed my requests-cache example above:
Responseto be subclassed as anattrs(or equivalent) dataclass #74175 remaining items
bastimeyer commentedon May 11, 2026
I've noticed some issues with the current definition of
ParamsType. I haven't followed this thread since my last post after which I fixed all the issues I ran into, so I'm not sure if this has been brought to attention yet.ParamsTypecurrently definesMapping[_ParamsMappingKeyType, _ParamsMappingValueType]as one of the union items, which is a problem, because the key type is invariant inMapping.The first example works, while the second one results in typing issues (checked via ty/mypy):
Typeshed defines it with a
SupportsItemsprotocol, similar to yours:requests/src/requests/_types.py
Lines 30 to 32 in dc9dbdf
nateprewitt commentedon May 11, 2026
Great find, @bastimeyer! I've put up #7426 which should match more closely to what typeshed is doing and it brings the API closer to our
Iterableinputs. Let me know if you think anything else is missing, otherwise we'll plan to merge that change.bastimeyer commentedon May 11, 2026
The
SupportsItemsProtocol changes in #7426 seem fine. At least they fix my issue when installing from that PR branch. Thanks.But what I just noticed is, why is there a dedicated
GetKwargsthat's excluding theparamskeyword from GET requests? GET requests should also allowparams, so query strings can be built from a mapping.RequestKwargsincludes it, but it's explicitly not used forget().requests/src/requests/_types.py
Lines 158 to 167 in dc9dbdf
nateprewitt commentedon May 11, 2026
Also a good find, there's actually asymmetry on the
Session.get()API vs therequests.get()API. The kwarg types were built onapi.pywhereparamsis explicit and then ported over tosession.pywhich is missing the same parameter. I'll get that cleaned up.Edit: Should be addressed in #7429.
chore: Check out inline type annotations in Requests
nateprewitt commentedon May 11, 2026
Adding a quick comment here to demarcate pre and post release of 2.34.0. Anyone arriving here after this comment, please provide a succinct description of what your input is, the affected APIs, and what you think typing should be. We'll review those on a case-by-case basis.
Known Issues
Requestclassprepare_bodystream detection for__getattr__-based file wrappers #7433jsonargument typing doesn't satisfy invariance requirementsrefactor: Update types to match new inline type annotations in Requests
refactor: Update types to match new inline type annotations in Requests
gothicVI commentedon May 12, 2026
With
Response.reasonbeing explicitly initialized asNoneand typed accordingly as union ofstr | Nonethis breaks type checkers that infer their types mainly from the source likepyright. This happens in our code base where we have custom exceptions we raise and that get initialized with the reason as astr.Interestingly with
types-requestsinstalledmypy,ty, andpyright are fine with us simply passingresponse.reasoninto our custom exception. Onlypyright` complains aboutAre you planning to address this or should we handle this?
I also noticed that in the
Response.raise_for_statusmethod there's no check forNonesuch that thereasonvariable in thehttp_error_msgcould be printed as empty.nateprewitt commentedon May 12, 2026
Thanks for the report, @gothicVI. Reason is inherently optional (even at the HTTP Protocol level) and as you noted initialized as
None. That said, we've smoothed this out for otherResponseattributes like status_code where it's almost neverNone. Let me think about this one for a bit today and we'll make a decision on if we clean this up in 2.34.1. Initial testing with urllib3 is showing they are normalizing missing values to''which is the primary path Requests users are going through.nateprewitt commentedon Jun 5, 2026
Closing this out since we haven't had any notable feedback in the last few weeks. Anything new can be raised as its own issue. Thanks again everyone who gave feedback and help with reviewing!