Skip to content

Add inline types to Requests#7272

Draft
nateprewitt wants to merge 2 commits intomainfrom
inline_types_rfc
Draft

Add inline types to Requests#7272
nateprewitt wants to merge 2 commits intomainfrom
inline_types_rfc

Conversation

@nateprewitt
Copy link
Member

@nateprewitt nateprewitt commented Mar 18, 2026

Add inline type annotations

Important

We want feedback from people who actively maintain projects that depend on Requests or use it heavily. Please share your experience testing this against your code in the linked issue.

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.

Overview

This PR adds type annotations throughout Requests to codify the type contract alongside the code. This is a path for what inline typing in Requests could look like. The goals are to establish what APIs are intended to support, make more reasoned decisions about code changes, and make using Requests in a modern development environment easier.

The package passes strict type checking with pyright, with the caveat that a few decisions on internals have been deferred for discussion by the maintainers and broader community. Right now, the intended behavior for those is undefined.

Related issue: RFC: Adding inline type annotations to Requests
Currently tracked TODOs: https://gist.github.com/nateprewitt/3e65fc8c3e9db3d5629ec1daf1f993f8

How to verify locally

pip install pyright typing-extensions
python -m pyright src/requests/

Should report 0 errors. typing-extensions is needed on Python 3.10-3.12.

Main design decisions

  • cast() over assert/isinstance for type narrowing to avoid breaking downstream code at runtime
  • New _types.py module centralizing Protocols, type aliases, and TypeVars
  • SupportsRead/SupportsItems as @runtime_checkable Protocols replacing hasattr checks
  • is_prepared() as a TypeIs guard that's a no-op at runtime (always returns True)
  • ~100 type suppressions, each documented with an inline PR comment

Runtime impact

A full catalog of every runtime-impacting change is available here. The behavioral changes and bug fixes are the ones worth scrutinizing. The rest is mechanical.

Open discussion points

  • str vs bytes URL handling: Requests has historically accepted both when Python 2.7 was primary usage. These types currently declare str only. This needs a decision on whether to fix existing broken bytes behavior or formalize the str-only contract.
  • yield_requests API: Needs documentation and a decision on how this should even work in practice. This API decision came through without review and I don't think there's any realistic usage outside of the library.

What's NOT in this PR

  • mypy compatibility: Developed against pyright. If we do add mypy, that will be in a separate follow-up. Public API types should still work for mypy users.
  • Public API changes: No signature changes, no new exceptions, no runtime assertions.
  • Test type-checking: Tests are not under strict mode. There are a number of tests that intentionally violate the type contract. We'll address those once we have a final plan here.

Lost commits from 4bd79e3...0e4ae38 in the squash. This readds them.
@nateprewitt nateprewitt added this to the Soon™ milestone Mar 18, 2026


def request(method, url, **kwargs):
def request(method: str, url: str, **kwargs: Any) -> Response:
Copy link

@cthoyt cthoyt Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it would be a lot of work, but annotating the kwargs with an Unpack[KwargsDict] where KwargsDict is a typing.TypedDict would be great!

I hacked this into my own code that interacts with requests code to make my usage more safe:

Copy link

@cthoyt cthoyt Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I am willing to PR this if you think it's a good idea / if you think an external contrib is better/easier than doing it yourself

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for comment, @cthoyt! This is a good callout because it is a delta from typeshed that should've been in the overview.

I played with Unpack for this but put that on pause because it got kind of messy. From what I was able to come up with we're going to need several different TypedDicts to accomplish it. Essentially one per signature, it's definitely doable, just clunky.

If you have ideas to simplify the approach, I'd love to hear more. This is something that needs an answer before we ship.

Copy link

@cthoyt cthoyt Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing worth noting is that you can have (multiple) inheritance on typed dicts. this can make it easier to factor out similarities between typed dict definitions for several functions

_ParamsMappingValueType: TypeAlias = (
str | bytes | int | float | Iterable[str | bytes | int | float] | None
)
ParamsType: TypeAlias = (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be lovely if these type hints were available through a "public" interface, since I've often written code that wraps a requests call, where I would want to use the ParamsType to annotate my own code

I say public in quotes because I could import this, but it feels wrong

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree having some form of public hints for the top-level APIs is potentially warranted. I started very conservative here because often times new code goes into Requests and immediately comes someone else's critical dependency. I don't want to create a binding contract that people start surfacing in their code, and then "break" when we need to update/tweak it.

I think once we're really happy with the types, the main argument parameters could be surfaced. I've deferred that for now until we can make a more informed decision. Presumably everything calling in is already typed sufficiently.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair! thank you for doing this and considering carefully :)

method=request.method,
url=url,
body=request.body,
body=request.body, # type: ignore[arg-type] # urllib3 stubs don't accept Iterable[bytes | str]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed with urllib3/urllib3#3799.

Copy link

@srittau srittau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding type hints! As a typeshed maintainer, I'm always glad to see annotations being added directly to packages. Less work for me , but more importantly as better user experience.

Also a general note: Some kwargs arguments could probably be accurately annotated using Unpack[_SomeTypedDict]. See for example the ast.pyi module in typeshed.

You could also consider marking constants as Final. This sometimes even makes type annotations easier. For example from below: Instead of

DEFAULT_PORTS: dict[str, int] = {"http": 80, "https": 443}

you could use

DEFAULT_PORTS: Final = {"http": 80, "https": 443}

At mypy infers the same type, although in some cases type checkers can even infer literals.

Some specific suggestions below.

Comment on lines +18 to +21
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider using a version matrix here. This could flag removals, API changes, and deprecations in CPython's stdlib early. Maybe just using the oldest and latest CPython version supported by requests would be enough as a smoke test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. I started with the minimum because we've generally tested floors but I agree having something for the latest version also makes sense to catch breakages as we add support.

Comment on lines +24 to +26
@runtime_checkable
class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ...) -> _T_co: ...
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider conditionally using io.Reader when using Python 3.14+. It is nearly identically defined, with one difference (pos-only length argument) that I would suggest adding:

Suggested change
@runtime_checkable
class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ...) -> _T_co: ...
@runtime_checkable
class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ..., /) -> _T_co: ...

Comment on lines +35 to +36
HookType = Callable[["Response"], Any]
HooksInputType = Mapping[str, "Iterable[HookType] | HookType"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these be marked as TypeAliases?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, also a good call out. I'll get those added.


TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None
ProxiesType: TypeAlias = MutableMapping[str, str]
HooksType: TypeAlias = dict[str, list["HookType"]] | None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: HookType doesn't need to be quotes as it's defined before in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout, the Hook* types were originally at the bottom of the file, but got lifted just before I posted this.

@nateprewitt
Copy link
Member Author

Really appreciate the response and your time looking at this, @srittau! I can get most of those addressed quick. The unpack piece I covered quickly with Chris in #7272 (comment). That's definitely a delta from typeshed we'll want to cover. The initial attempt on that got messy pretty quickly so I sidelined it. We'll get that addressed before shipping this though.

@nateprewitt
Copy link
Member Author

I've added https://gist.github.com/nateprewitt/3e65fc8c3e9db3d5629ec1daf1f993f8 to track our current checklist before merging this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants