2025, Oct 03 15:00
WindowsDowndate pathlib breakage: fixing WindowsPath._from_parts on Python 3.11–3.13
Troubleshooting WindowsDowndate on newer Python: resolve pathlib _from_parts errors, align with Python 3.11 or add constructor helpers for 3.12/3.13. See how.
When you pull a security research tool and it immediately trips over the standard library, the root cause is often a version mismatch or a stray import. That’s exactly what happens with WindowsDowndate when the code tries to rely on pathlib internals that differ between Python releases.
What breaks and where
The failure surfaces in a helper module where a subclass of WindowsPath is created and a private constructor helper is pulled from pathlib. In the problematic setup it looks like this:
import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath, _from_parts
class PathPlus(WindowsPath):
    """
    WindowsPath extension that expands env vars and exposes NT path.
    """
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)
This variant imports _from_parts from pathlib and invokes cls._from_parts(...) in __new__. That’s where things go sideways.
Why it fails
In the repository’s current code, the import is only from pathlib import WindowsPath. Pulling _from_parts from pathlib is unnecessary and incorrect. In Python 3.11, _from_parts exists as an internal method on PurePath, not as a top-level importable symbol. In Python 3.12 and 3.13, that internal changed and the method isn’t present there as it was before. The project itself explicitly states it was tested with Python 3.11.9, which aligns with the implementation that expects the internal method to be available on the class hierarchy rather than imported.
A full traceback would confirm whether you’re hitting an import failure or an attribute error, but the mismatch is clear from the code and version notes.
Two practical fixes
The first path is to align your environment with what the project expects. If you run Python 3.11, simply remove the stray import and rely on the inherited method. The second path is to keep using 3.12/3.13 and reintroduce the missing pieces inside your subclass, as shown below.
Fix for Python 3.11: drop the bad import
Keep the class the same and remove _from_parts from the import line. The inherited internals are enough in the tested environment.
import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath
class PathPlus(WindowsPath):
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)
This matches the repository’s current import and is consistent with “tested with python 3.11.9”.
Forward-compat option for Python 3.12/3.13
If you want to stay on newer Python, you can provide the missing constructor helpers in your subclass. This mirrors what 3.11 relied on internally and also includes the companion argument parser that the constructor depends on.
import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath, PurePath
class PathPlus(WindowsPath):
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @classmethod
    def _from_parts(cls, items):
        inst = object.__new__(cls)
        drv, root, parts = inst._parse_args(items)
        inst._drv = drv
        inst._root = root
        inst._parts = parts
        return inst
    @classmethod
    def _parse_args(cls, items):
        chunks = []
        for piece in items:
            if isinstance(piece, PurePath):
                chunks += piece._parts
            else:
                fs_val = os.fspath(piece)
                if isinstance(fs_val, str):
                    chunks.append(str(fs_val))
                else:
                    raise TypeError(
                        "argument should be a str object or an os.PathLike "
                        "object returning str, not %r" % type(fs_val)
                    )
        return cls._flavour.parse_parts(chunks)
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)
This embeds the internal constructor path so you don’t depend on pathlib’s private surface staying the same across versions.
Why this matters
Projects that lean on private or semi-private internals of the standard library are brittle across minor Python upgrades. Here, a subtle difference between Python 3.11 and 3.12/3.13 around pathlib internals is enough to stop the tool at import time. Confirming the exact failure with a complete traceback and matching your environment to the repository’s stated baseline prevents unnecessary debugging time. If you must stay on newer interpreters, isolating and vendoring the minimum logic you rely on can keep you unblocked while staying close to upstream behavior.
Takeaways
Verify the repository version you have matches the current code, especially imports. Align Python to the version the project lists as tested—in this case, 3.11.9—when you want the quickest success path. If you cannot downgrade, embed the small set of helpers the code expects, as shown above, to restore compatibility on 3.12/3.13. And always keep the full traceback at hand; it will make mismatches like this immediately obvious.
The article is based on a question from StackOverflow by oz hagevermeleh and an answer by furas.