Skip to content

feat: annotate HttpRequest as readable, HttpResponse as writable#3218

Open
alexei wants to merge 3 commits into
typeddjango:masterfrom
alexei:feat-readable_request_writable_response
Open

feat: annotate HttpRequest as readable, HttpResponse as writable#3218
alexei wants to merge 3 commits into
typeddjango:masterfrom
alexei:feat-readable_request_writable_response

Conversation

@alexei

@alexei alexei commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

HttpRequests are readable, while HttpResponses are writable. It's insufficient that they have read and write methods respectively, as one would have to use a Protocol. However Reader and Writer are already available in the io package and are better suited.

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you don't have to inherit from a protocol to make use of it. HttpRequest already should be compatible with Readable and HttpResponse should be compatible with Writable.

If you want, you can submit a test case for that.

@alexei

alexei commented Mar 24, 2026

Copy link
Copy Markdown
Contributor Author

It's my fault for not explaining this. I found this while writing to a response with a function that accepted a writable file-thing i.e. write_to_resp(content: bytes, output: Writer). I tried both IO and Writer on it and ty complained -- "Expected Writer, found HttpResponse". For file-like objects the Internet says to use IO, but the somewhat official recommendation is to use a Protocol, and that worked. Under these circumstances it should be reasonable to expect everyone to use IO or better yet Writer as they exist in the standard library.

@sobolevn

Copy link
Copy Markdown
Member

io.Reader or typing_extensions.Reader is just:

@runtime_checkable
class Reader(Protocol[T_co]):
    """Protocol for simple I/O reader instances.

    This protocol only supports blocking I/O.
    """

    __slots__ = ()

    @abc.abstractmethod
    def read(self, size: int = ..., /) -> T_co:
        """Read data from the input stream and return it.

        If *size* is specified, at most *size* items (bytes/characters) will be
        read.
        """

So, you don't need to modify HttpRequest to use it. Because it is already supported, HttpRequest.read exists here

def read(self, n: int | None = -1, /) -> bytes: ...
and matches the Reader protocol.

As I said before you can add a test case to typesafety/assert_type that they indeed match:

to_read: Reader = HttpRequest()

And the same for HttpResponse.

@alexei

alexei commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

I forgot about this -- sorry! Yes, Reader and Writer are Protocols in typing_extensions on Python <=3.13:

https://github.com/python/typing_extensions/blob/83caa5908b408560b7e30d60052c2e4da31b0556/src/typing_extensions.py#L979

but not in io in Python >=3.14:

https://github.com/python/cpython/blob/b87590fd275b992364b716ea79341fd6069009c5/Lib/io.py#L107

or typing_extensions on Python >=3.14:

https://github.com/python/typing_extensions/blob/83caa5908b408560b7e30d60052c2e4da31b0556/src/typing_extensions.py#L974-L976

--

After writing this comment I realised I discovered this under Python 3.11, which makes it weird. I'll need to do some tests.

@alexei

alexei commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

The same code:

from django.http import HttpRequest, HttpResponse
from typing_extensions import Writer


def write_to_response(output: Writer[str], contents: str) -> None:
    output.write(contents)


def view(request: HttpRequest) -> HttpResponse:
    resp = HttpResponse()
    write_to_response(resp, "Hello, World!")
    return resp

Fails on Python 3.10-3.14:

> uv run ty check
error[[invalid-argument-type](https://ty.dev/rules#invalid-argument-type)]: Argument to function `write_to_response` is incorrect
  --> main.py:11:23
   |
 9 | def view(request: HttpRequest) -> HttpResponse:
10 |     resp = HttpResponse()
11 |     write_to_response(resp, "Hello, World!")
   |                       ^^^^ Expected `Writer[str]`, found `HttpResponse`
12 |     return resp
   |
info: Function defined here
 --> main.py:5:5
  |
5 | def write_to_response(output: Writer[str], contents: str) -> None:
  |     ^^^^^^^^^^^^^^^^^ ------------------- Parameter declared here
6 |     output.write(contents)
  |
info: rule `invalid-argument-type` is enabled by default

Found 1 diagnostic

... until I annotate HttpResponse with Writer.

While I think it should fail on Python 3.14 because it's an ABC, I must admit I'm confused about the earlier versions where they're Protocols.

@alexei

alexei commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

And I just learned Django's HttpResponse is not compatible with Writer because it doesn't return anything, so the whole premise is wrong 😞

https://github.com/django/django/blob/6f030e8e5d13ee94bf45d4322c17ca7c2d8aaffb/django/http/response.py#L426-L427

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants