Skip to content

urljoin() undocumented behavior change in Python 3.14. #125926

Description

@felixxm

Bug report

Bug description:

Django is tested with the earliest alpha versions. We noticed a behavior change in the urllib.parse.urljoin() that is used in a few places in Django, e.g. for staticfiles or build-in storages.

Python 3.14.0a1:

>>> from urllib.parse import urljoin
>>> urljoin("/static/", "admin/img/icon-addlink.svg")
admin/img/icon-addlink.svg

Python 3.13 and earlier:

>>> from urllib.parse import urljoin
>>> urljoin("/static/", "admin/img/icon-addlink.svg")
/static/admin/img/icon-addlink.svg

Is this an intentional change?

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

Activity

  1. nineteendo commented on Oct 24, 2024

    @nineteendo
    Contributor

    I'm assuming this is bug fixed by #123273.

  2. felixxm commented on Oct 24, 2024

    @felixxm
    ContributorAuthor

    I'm assuming this is bug fixed by #123273.

    #123273 is included in 3.14.0a1, so (unfortunately) it cannot address this issue.

  3. nineteendo commented on Oct 24, 2024

    @nineteendo
    Contributor

    Sorry, I meant this might be caused by it.

  4. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.14bugs and security fixes
    on Oct 24, 2024
  5. felixxm commented on Oct 25, 2024

    @felixxm
    ContributorAuthor

    Sorry, I meant this might be caused by it.

    I can bisect and confirm (or not) during the weekend.

  6. hugovk commented on Oct 25, 2024

    @hugovk
    Member

    Yes, git bisect points to fc897fc from #123273.

    fc897fcc01964649f023e0baa4c95d142e4e8a10 is the first bad commit
    commit fc897fcc01964649f023e0baa4c95d142e4e8a10
    Date:   Sat Aug 31 12:42:08 2024 +0300
    
        gh-76960: Fix urljoin() and urldefrag() for URIs with empty components (GH-123273)
    
        * urljoin() with relative reference "?" sets empty query and removes fragment.
        * Preserve empty components (authority, params, query, fragment) in urljoin().
        * Preserve empty components (authority, params, query) in urldefrag().
    
        Also refactor the code and get rid of double _coerce_args() and
        _coerce_result() calls in urljoin(), urldefrag(), urlparse() and
        urlunparse().
    
     Lib/test/test_urlparse.py                                              |  87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
     Lib/urllib/parse.py                                                    | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------
     Misc/NEWS.d/next/Library/2024-08-23-22-01-30.gh-issue-76960.vsANPu.rst |   5 +++++
     3 files changed, 140 insertions(+), 52 deletions(-)
     create mode 100644 Misc/NEWS.d/next/Library/2024-08-23-22-01-30.gh-issue-76960.vsANPu.rst
    
  7. added 2 commits that reference this issue on Oct 25, 2024
  8. serhiy-storchaka commented on Oct 25, 2024

    @serhiy-storchaka
    Member

    urljoin() was updated to match the reference resolution algorithm in RFC 3986, Section 5. But the algorithm requires the base URI to be an absolute URI. This is why there were no tests for relative base URI. But urljoin() was used with relative base URIs, and it should continue to return sensible result for relative base URI, even if this goes beyond the application of RFC 3986.

  9. added a commit that references this issue on Oct 25, 2024
  10. added a commit that references this issue on Nov 7, 2024
  11. added a commit that references this issue on Dec 8, 2024
  12. added a commit that references this issue on Jan 12, 2025
  13. haampie commented on Nov 29, 2025

    @haampie
    Contributor

    Another difference is the following:

    Python <3.14

    >>> urljoin("file:", "/abc")
    'file:///abc'
    

    Python 3.14.0

    >>> urljoin("file:", "/abc")
    'file:/abc'
    

    Is that expected?

  14. serhiy-storchaka commented on Nov 29, 2025

    @serhiy-storchaka
    Member

    Yes, this is expected. Use "file://" if you want to get an URL with an empty (instead of absent) authority.

    Semantically, "file:/abc" and "file:///abc" are equivalent, the difference is only in the representation, and in 3.14 you can control what representation you want to get by using "file:" or "file://".

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

Metadata

Metadata

Labels

3.14bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions