Skip to content

/a/** is understood by Path.full_match as /a followed by multiple segments, at least one #139580

Open

Description

@macmarrum

Bug report

Bug description:

/a/** is understood by Path.full_match as "/a followed by multiple segments, at least one", instead of "/a followed by multiple segments, including zero".

From https://docs.python.org/3/library/pathlib.html#pathlib-pattern-language

** (entire segment)
Matches any number of file or directory segments, including zero.
# one segment per **
Path('/a/b').full_match('/a/**') # True
# zero segments per **
Path('/a').full_match('/a/**') # False - expected True
# interestingly
Path('/').full_match('/**') # True

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    pendingThe issue will be closed if no feedback is provided
    on Oct 5, 2025
  2. picnixz commented on Oct 5, 2025

    @picnixz
    Member

    Path('/a').full_match('/a/**') # False - expected True

    I was wrong before, and /a/ is also not matched. I don't know if it's a bug in the code or in the docs.

    cc @barneygale

  3. removed
    pendingThe issue will be closed if no feedback is provided
    on Oct 5, 2025
  4. barneygale commented on Oct 5, 2025

    @barneygale
    Contributor

    Path('/a').full_match('/a/**') # False - expected True

    I think this is expected, because the path does not begin with /a/. The docs could be clearer on this point.

    I was wrong before, and /a/ is also not matched. I don't know if it's a bug in the code or in the docs.

    pathlib.Path removes trailing slashes so Path('/a/') becomes Path('/a') and fails to match.

  5. picnixz commented on Oct 5, 2025

    @picnixz
    Member

    Oh, so I wasn't wrong before! I don't know how the docs could be improved though.

  6. added
    docsDocumentation in the Doc dir
    and removed
    type-bugAn unexpected behavior, bug, or error
    stdlibStandard Library Python modules in the Lib/ directory
    on Oct 5, 2025
  7. macmarrum commented on Oct 5, 2025

    @macmarrum
    Author

    The ** pattern has different interpretations in glob and full_match:

    1. In Path('/a').glob('**'), it means "all files and directories under '/a', including '/a' itself"
    2. However, Path('/a').full_match('/a/**') interprets it differently, which I find unexpected
    from pathlib import Path
    
    from tempfile import gettempdir, mktempi
    
    temp = Path(gettempdir()) / mktemp()
    temp_a = temp / 'a'
    temp_a.mkdir(parents=True)
    temp_a_1 = temp_a / '1.txt'
    temp_a_1.touch()
    expected = {temp, temp_a, temp_a_1}
    actual = {*temp.glob('**')}
    print('Actual:', [f"{p}" for p in sorted(actual)])
    assert actual == expected, f"\nExpected: {[f"{p}" for p in sorted(expected)]}"
    
    # Actual: ['/tmp/tmpmcdga9my', '/tmp/tmpmcdga9my/a', '/tmp/tmpmcdga9my/a/1.txt']
  8. barneygale commented on Oct 5, 2025

    @barneygale
    Contributor

    In Path('/a').glob('**'), it means "all files and directories under '/a', including '/a' itself"

    If pathlib preserved trailing slashes, then the glob would yield '/a/', and that would match '/a/**'. The inconsistency comes from pathlib stripping trailing slashes unfortunately...

  9. barneygale commented on Oct 5, 2025

    @barneygale
    Contributor

    It's probably too late to change pathlib.Path to preserve trailing slashes - it would cause a lot of breakage.

    But I'm open to the idea of a new class, perhaps pathlib.SimplePath, that is merely an object-orientated version of os.path without any path normalization. That would look like:

    >>> list(SimplePath('/tmp/a').glob('**'))
    [SimplePath('/tmp/a/'), SimplePath('/tmp/a/1.txt')]
    >>> SimplePath('/tmp/a/').full_match('/tmp/a/**')
    True
    >>> SimplePath('/tmp/a/1.txt').full_match('/tmp/a/**')
    True

    If anyone wants to pursue this, I'd recommend starting a topic in the Ideas forum.

  10. macmarrum commented on Oct 5, 2025

    @macmarrum
    Author

    If pathlib preserved trailing slashes, then the glob would yield '/a/', and that would match '/a/**'. The inconsistency comes from pathlib stripping trailing slashes unfortunately...

    While this explains the technical reason behind the behavior, it's essentially an implementation detail. From a user's perspective, the functionality shouldn't depend on such nuances; Path('/a').full_match('/a/**') returning False remains counter-intuitive, in my opinion.
    If modifying full_match's behavior is impractical, the documentation should at least clearly explain how full_match interprets the ** pattern, since it differs from glob's interpretation.

  11. added 2 commits that reference this issue on Jun 14, 2026
  12. harjothkhara commented on Aug 18, 2026

    @harjothkhara
    Contributor

    There's a docs PR open for this: #151479. It adds a /a/** example and a note on the trailing-slash normalization.

  13. added 2 commits that reference this issue on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions