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
Copy link
Copy Markdown
Contributor

@alexei alexei commented Mar 24, 2026

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.

Copy link
Copy Markdown
Member

@sobolevn sobolevn left a comment

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
Copy link
Copy Markdown
Contributor Author

alexei commented Mar 24, 2026

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
Copy link
Copy Markdown
Contributor Author

alexei commented Apr 10, 2026

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
Copy link
Copy Markdown
Contributor Author

alexei commented Apr 10, 2026

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
Copy link
Copy Markdown
Contributor Author

alexei commented Apr 10, 2026

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

Copy link
Copy Markdown
Member

@sobolevn sobolevn left a comment

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