[Django] #36664: Python 3.15 compatibility.

30 views
Skip to first unread message

Django

unread,
Oct 14, 2025, 1:00:18 PM10/14/25
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz | Owner: Mariusz Felisiak
Felisiak |
Type: New | Status: assigned
feature |
Component: Core | Version: dev
(Other) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
Python 3.15 final is scheduled for October 2026 (see
​​[https://peps.python.org/pep-0790/ PEP 790]). This is a tracking ticket
for compatibility fixes for Django submitted in the meantime.

Django 6.1 will be the first version to support Python 3.15, because
Django 6.0 will end the mainstream support in August 2026.
--
Ticket URL: <https://code.djangoproject.com/ticket/36664>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Oct 14, 2025, 1:15:00 PM10/14/25
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* stage: Unreviewed => Accepted

--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:1>

Django

unread,
Oct 21, 2025, 4:36:37 AM10/21/25
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* stage: Accepted => Someday/Maybe

--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:2>

Django

unread,
Oct 22, 2025, 9:36:24 AM10/22/25
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by GitHub <noreply@…>):

In [changeset:"5e2bbebed9f36bb9d15f168444e7982287761877" 5e2bbebe]:
{{{#!CommitTicketReference repository=""
revision="5e2bbebed9f36bb9d15f168444e7982287761877"
Refs #36664 -- Added Python 3.15 to daily builds.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:3>

Django

unread,
May 8, 2026, 11:52:59 AMMay 8
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls <jacobtylerwalls@…>):

In [changeset:"e7f539f813bd56a71ca3c1fbf379f47691002086" e7f539f]:
{{{#!CommitTicketReference repository=""
revision="e7f539f813bd56a71ca3c1fbf379f47691002086"
Refs #36712, #36664 -- Used annotation_format parameter of
getfullargspec() on Python 3.15.

https://github.com/python/cpython/pull/149457
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:4>

Django

unread,
May 12, 2026, 3:50:47 PMMay 12
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* cc: Mike Edmunds (added)

Comment:

Hi Mike,

Thanks to you, the future referenced in `EmailBackend.prep_address()` has
arrived:

{{{#!py
if not force_ascii:
# Non-ASCII local-part is valid with SMTPUTF8. Remove once
# https://github.com/python/cpython/issues/81074 is fixed.
defects.discard("local-part contains non-ASCII characters)")
}}}

We have on the 3.15 builds:

{{{#!py
======================================================================
ERROR: test_rejects_non_ascii_local_part
(mail.test_backends.SMTPBackendTests.test_rejects_non_ascii_local_part)
The SMTP EmailBackend does not currently support non-ASCII local-parts.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/jwalls/django/tests/mail/test_backends.py", line 832, in
test_rejects_non_ascii_local_part
backend.send_messages([email])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/Users/jwalls/django/django/core/mail/backends/smtp.py", line 138,
in send_messages
sent = self._send(message)
File "/Users/jwalls/django/django/core/mail/backends/smtp.py", line 155,
in _send
self.connection.sendmail(from_email, recipients, message.as_bytes())
^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/unittest/mock.py",
line 697, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'sendmail'
}}}

It fails more informatively when I remove `spec=object()`:

{{{#!py
======================================================================
ERROR: test_rejects_non_ascii_local_part
(mail.test_backends.SMTPBackendTests.test_rejects_non_ascii_local_part)
The SMTP EmailBackend does not currently support non-ASCII local-parts.
----------------------------------------------------------------------
Traceback (most recent call last):
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2843, in _refold_without_ew
tstr.encode(encoding)
~~~~~~~~~~~^^^^^^^^^^
UnicodeEncodeError: 'ascii' codec can't encode character '\xf8' in
position 1: ordinal not in range(128)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/Users/jwalls/django/tests/mail/test_backends.py", line 832, in
test_rejects_non_ascii_local_part
backend.send_messages([email])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/Users/jwalls/django/django/core/mail/backends/smtp.py", line 138,
in send_messages
sent = self._send(message)
File "/Users/jwalls/django/django/core/mail/backends/smtp.py", line 155,
in _send
self.connection.sendmail(from_email, recipients, message.as_bytes())
~~~~~~~~~~~~~~~~^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/message.py",
line 214, in as_bytes
g.flatten(self, unixfrom=unixfrom)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/generator.py",
line 118, in flatten
self._write(msg)
~~~~~~~~~~~^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/generator.py",
line 201, in _write
self._write_headers(msg)
~~~~~~~~~~~~~~~~~~~^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/generator.py",
line 433, in _write_headers
folded = self.policy.fold_binary(h, v)
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/policy.py",
line 207, in fold_binary
folded = self._fold(name, value, refold_binary=self.cte_type=='7bit')
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/policy.py",
line 213, in _fold
return value.fold(policy=self)
~~~~~~~~~~^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/headerregistry.py",
line 251, in fold
return header.fold(policy=policy)
~~~~~~~~~~~^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 170, in fold
return _refold_parse_tree(self, policy=policy)
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2832, in _refold_parse_tree
_refold_with_ew(parse_tree, lines, maxlen, encoding, policy=policy)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2936, in _refold_with_ew
encoded_part = part.fold(policy=policy)[:-len(policy.linesep)]
~~~~~~~~~^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 170, in fold
return _refold_parse_tree(self, policy=policy)
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2832, in _refold_parse_tree
_refold_with_ew(parse_tree, lines, maxlen, encoding, policy=policy)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2936, in _refold_with_ew
encoded_part = part.fold(policy=policy)[:-len(policy.linesep)]
~~~~~~~~~^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 170, in fold
return _refold_parse_tree(self, policy=policy)
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2834, in _refold_parse_tree
_refold_without_ew(parse_tree, lines, maxlen, encoding, policy=policy)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Library/Frameworks/Python.framework/Versions/3.15/lib/python3.15/email/_header_value_parser.py",
line 2854, in _refold_without_ew
raise errors.HeaderWriteError(
...<2 lines>...
)
email.errors.HeaderWriteError: Non-ASCII local-part 'nø' is invalid under
current policy setting (utf8=False)

----------------------------------------------------------------------
}}}

Should we be catching `email.errors.HeaderWriteError` and re-raising in
the code, or should we be adjusting the test for 3.15? And should we do
anything about the `if not force_ascii:` branch for now? Appreciate your
advice.
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:5>

Django

unread,
May 12, 2026, 4:30:31 PMMay 12
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls):

On another topic, we have a failure in `test_max_diff` also due to the new
`pprint` defaults. It's only a matter of doing this, because the output of
`assertDictEqual()` is now more compact...

{{{#!diff
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py
index e54b9ef624..18a35c018a 100644
--- a/tests/test_utils/tests.py
+++ b/tests/test_utils/tests.py
@@ -353,7 +353,7 @@ class AssertQuerySetEqualTests(TestCase):
)

def test_maxdiff(self):
- names = ["Joe Smith %s" % i for i in range(20)]
+ names = ["Joe Smith %s" % i for i in range(25)]
Person.objects.bulk_create([Person(name=name) for name in names])
names.append("Extra Person")
}}}

but they change
[https://github.com/python/cpython/issues/149189#issuecomment-4416150007
might be reverted], so let's wait.
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:6>

Django

unread,
May 13, 2026, 1:12:34 PMMay 13
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Mike Edmunds):

Replying to [comment:5 Jacob Walls]:
> We have on the 3.15 builds:
>
> {{{#!text
> ERROR: test_rejects_non_ascii_local_part
(mail.test_backends.SMTPBackendTests.test_rejects_non_ascii_local_part)
> The SMTP EmailBackend does not currently support non-ASCII local-parts.
> }}}
>
> [...]
>
> Should we be catching `email.errors.HeaderWriteError` and re-raising in
the code, or should we be adjusting the test for 3.15? And should we do
anything about the `if not force_ascii:` branch for now? Appreciate your
advice.

I'll open a PR to update the test. I ''think'' the right approach to
catching unsupported non-ASCII email usernames is:

* Let stdlib email report the problem when it's capable (i.e., 3.15+), as
a `HeaderWriteError: Non-ASCII local-part … is invalid under current
policy`. I know we try to avoid testing stdlib behavior, but given the
upstream flux in this area we might want to ensure ''something'' useful
gets reported to the user (without pinning our tests to the precise stdlib
error).
* Continue to raise (and test) our own `ValueError` in `prep_address()` if
stdlib email would incorrectly encode the email as something undeliverable
(i.e., before 3.15; there are currently no plans to backport the bugfix
from 3.15 due to compatibility concerns).

> It fails more informatively when I remove `spec=object()`:

There's actually no need for that test to mock the smtplib connection in
the first place; I'll remove it.
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:7>

Django

unread,
May 13, 2026, 2:28:47 PMMay 13
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Mike Edmunds):

Just a heads up that the markers in tests/requirements/py3.txt should
probably be `sys_platform` rather than `sys.platform`. Also, there's no
numpy wheel available for 3.150b1 on macOS, and the build requirements
seem complex, so I just disabled it like was already done for win32.

{{{#!patch
Index: tests/requirements/py3.txt
===================================================================
diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt
--- a/tests/requirements/py3.txt (revision
05a5244b8924c47cc5b1332dd15aaa1be6abbe1e)
+++ b/tests/requirements/py3.txt (date 1778696468353)
@@ -6,8 +6,8 @@
docutils >= 0.22
geoip2 >= 4.8.0
jinja2 >= 2.11.0
-numpy >= 1.26.0; sys.platform != 'win32' or python_version < '3.15'
-Pillow >= 10.1.0; sys.platform != 'win32' or python_version < '3.15'
+numpy >= 1.26.0; sys_platform != 'win32' and sys_platform != 'darwin' or
python_version < '3.15'
+Pillow >= 10.1.0; sys_platform != 'win32' or python_version < '3.15'
# pylibmc/libmemcached can't be built on Windows.
pylibmc; sys_platform != 'win32'
pymemcache >= 3.4.0
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:8>

Django

unread,
May 13, 2026, 3:34:20 PMMay 13
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mike Edmunds):

* has_patch: 0 => 1

Comment:

Email test updates: https://github.com/django/django/pull/21284.

(Don't know whether I'm supposed to mark "has patch" for refs PRs, but
figured it wouldn't hurt to put it in the review queue.)
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:9>

Django

unread,
May 14, 2026, 7:49:34 AMMay 14
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls <jacobtylerwalls@…>):

In [changeset:"25cf1cbb1cb92c6c57b76cc43d697cfdc61f568f" 25cf1cb]:
{{{#!CommitTicketReference repository=""
revision="25cf1cbb1cb92c6c57b76cc43d697cfdc61f568f"
Refs #36664 -- Updated SMTP EmailBackend tests for Python 3.15.

Versions of Python prior to 3.15 would incorrectly encode non-ASCII
email addresses using rfc2047, resulting in undeliverable email. The
SMTP EmailBackend detects and prevents that (#35713). Python 3.15 fixes
that behavior (CPython issue gh-122476).

Updated test_rejects_non_ascii_local_part() to feature-detect the fix
(in case it is backported) and check for a representative section of
the Python error message if so; otherwise test for the SMTP EmailBackend
workaround.

Updated comments to clarify need and requirement.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:10>

Django

unread,
May 14, 2026, 7:49:46 AMMay 14
to django-...@googlegroups.com
#36664: Python 3.15 compatibility.
-------------------------------------+-------------------------------------
Reporter: Mariusz Felisiak | Owner: Mariusz
| Felisiak
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Someday/Maybe
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* has_patch: 1 => 0

--
Ticket URL: <https://code.djangoproject.com/ticket/36664#comment:11>
Reply all
Reply to author
Forward
0 new messages