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.