Skip to main content

winseam

Your Python tool works. Then someone runs it on Windows, and the output is Тест — café, or shutil.rmtree refuses to delete a Git clone, or json.load(open(path)) raises UnicodeDecodeError on a file that is plainly valid UTF-8.

None of that is bad luck. Windows has more than one default encoding at a time, and the standard library decodes every child process with the wrong one.

$ winseam doctor
  locale code page    : cp1251   (what subprocess text=True decodes with)
  console code page   : cp866    (what a console child writes)

Two code pages, one process tree. So there is no single encoding= that is correct for all of your children:

child subprocess.run(text=True) winseam.run what it actually wrote
python -c "print('Тест — café')" Тест — cafГ© Тест — café utf-8
cmd /c echo Тест ’Ґбв Тест cp866
git --version ok ok utf-8
from winseam import run

result = run(["git", "log", "-1", "--format=%s"])
print(result.stdout)     # correct text, whoever the child was
print(result.encoding)   # 'utf-8' -- and it tells you, instead of guessing silently

Install

pip install winseam

No dependencies. Off Windows every function is a thin pass-through with identical behaviour, so the call site is written once.

Why this does not go away in Python 3.15

PEP 686 makes UTF-8 the default in Python 3.15, which fixes open() and text=True for Python's own data. It does not touch cmd, git, npm, ffmpeg or az: they keep writing the console code page. After 3.15 their output stops being silent mojibake and starts being UnicodeDecodeError instead.

The seam moves. It does not close. winseam.run decides per child — it configures a Python child through PYTHONUTF8/PYTHONIOENCODING rather than guessing at it, and reads a console child in the code page a console child writes.

What it gives you

The process boundary

run(args, encoding=None, input=None, check=False, **subprocess_kwargs)

A CompletedProcess with str output, plus .encoding and .encodings (per stream). text=True is refused with a message rather than accepted and ignored. A str passed as input is encoded for that particular child, and never gets a BOM — PowerShell 5.1 adds one to the pipe it hands a child, and that byte has broken every header parser it has ever met.

The filesystem

read_text(path)      # utf-8, and a BOM written by another tool is not your problem
write_text(path, s)  # utf-8, no BOM, '\n' stays '\n'
read_json(path)      # the one that crashes in every bug report
write_json(path, d)  # non-ASCII stays readable, not Тест

rmtree(path)         # deletes read-only files -- i.e. can delete a .git directory
replace(src, dst)    # survives a scanner holding the destination for a moment
unlink(path)         # clears the read-only bit, waits out a transient handle

Measured on a stock Windows 11 box, where six of ten standard-library operations fail:

rmtree on a read-only file     PermissionError [WinError 5]    <- how Git stores every object
delete a file someone opened   PermissionError [WinError 32]
replace a file someone opened  PermissionError [WinError 5]
open()/Path.read_text()        UnicodeDecodeError (cp1251)
subprocess text=True           mojibake
NamedTemporaryFile reopen      PermissionError

Find them before your users do

$ winseam audit .

src/runner.py
    41  WS001  subprocess.run(text=True) without encoding= decodes the child with the ANSI
               code page; a UTF-8 child is mojibake and a console child is worse
        fix: winseam.run(...), or pass encoding= explicitly
    88  WS004  shutil.rmtree() without onexc= fails with PermissionError on read-only files,
               including every object in a .git directory
        fix: winseam.rmtree(path)

51 findings in 26 files
  WS001  5
  WS002  30
  WS003  16

It is a static pass, so it runs on Linux CI and on projects you do not own. --json for machine-readable output, --exit-zero for a non-blocking CI step. winseam doctor prints the code pages of the machine you are on, which is the first question on every Windows bug report and the one nobody can answer from memory.

code what it finds
WS001 subprocess(text=True) with no encoding=
WS002 open() in text mode with no encoding=
WS003 Path.read_text() / write_text() with no encoding=
WS004 shutil.rmtree() with no onexc=

What this does not claim

  • Single-byte code pages cannot be detected. cp866 bytes decode "successfully" as cp1251 and give mojibake. UTF-8 is self-validating, so it is tried first and is nearly always right; below it the ladder is an informed prior, which is exactly why .encoding is returned rather than kept private. Pass encoding= when you know better.
  • Retries do not beat a handle that is genuinely held. They wait out an indexer or a scanner, which is the usual cause; a file another process is really using still fails, and should, rather than hanging.
  • It is not a compatibility shim for everything. Four constructs, measured, that account for nearly every Windows text bug in Python tooling.

License

MIT

Metadata

Release files for winseam 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for winseam 0.1.0
File Size Uploaded
winseam-0.1.0.tar.gz 15.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for winseam 0.1.0
File Interpreter ABI Platform
winseam-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 31.4 kB

Release files / winseam-0.1.0.tar.gz

Download URL winseam-0.1.0.tar.gz
Size 15.8 kB
Tags Source
SHA-256 checksum
How to use checksums
6f7bbff9a448cf01ec726515a5ce58c56b1bc5b776c0c82c507c51833f67f748
BLAKE2b-256 checksum
How to use checksums
da2dd02a22ff1626b95c23857da9c7ace14511e5634eada56636333e3ac54fa5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release files / winseam-0.1.0-py3-none-any.whl

Download URL winseam-0.1.0-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
645feecd6f7abd7b6b4572f0f75d516d9736a5ef85bb8f5fa323949cdbcd530d
BLAKE2b-256 checksum
How to use checksums
d17ba7e37eb7ce9baffd4cfa5d99c2cb882804d9298ff6221e426c7d11403e4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page