Platform-specific behaviour
Case sensitivity, line endings, signal handling, and how to detect the current OS when conditional logic is unavoidable.
- Identify how filesystem case sensitivity differences hide bugs until Linux CI runs
- Explain Windows line-ending issues and use open(path, newline='') for portable CSV
- Know which signals are available on all platforms and which are Windows-only limitations
- Use sys.platform to write conditional code for genuine platform differences
Getting paths right is half the battle. The other half is understanding that the filesystem, the file format, and the process model behave differently across operating systems in ways that only surface on the wrong platform — often in CI, after you have shipped.
Case sensitivity
Linux filesystems are case-sensitive by default: Data.csv and data.csv are
different files. macOS and Windows default to case-insensitive: both names refer
to the same file.
This hides a specific class of bug. Code written on macOS that opens Data.csv
using the filename data.csv works locally but fails as a FileNotFoundError on
Linux CI. The fix is simple — be consistent. Always use the exact filename the file
was created with, and enforce a naming convention (all lowercase, hyphens not
spaces) in your project so there is never ambiguity.
Tools that accept user-supplied filenames should normalise casing explicitly if they need case-insensitive matching:
# Case-insensitive file lookup without relying on the filesystem
target = "data.csv"
found = next(
(p for p in Path(".").iterdir() if p.name.lower() == target.lower()),
None,
)Line endings
Windows uses \r\n (CRLF) line endings; Linux and macOS use \n (LF). Python's
text mode opens files with universal newline translation on, so reading is usually
fine. Writing is where it breaks: if you write a file in text mode on Windows, it
gets \r\n line endings that corrupt downstream Linux processes expecting LF.
For CSV files specifically, the csv module documentation requires opening files
with newline='' to prevent double-translation:
import csv
from pathlib import Path
# Correct on all platforms
with open(Path("output.csv"), "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)For non-CSV text files, open with newline="\n" to force LF output on all
platforms:
with open(path, "w", newline="\n") as f:
f.write(content)Git's core.autocrlf setting can silently convert line endings in the working
tree, which means a file that looks correct locally may have different endings
in a script the runner executes. Add a .gitattributes file with
*.csv text eol=lf to pin line endings for specific file types.
Signal handling
Unix signal handling is well-defined in Python. SIGINT (Ctrl-C) and SIGTERM
(graceful termination from a process manager) are available everywhere:
import signal, sys
def handle_sigterm(signum, frame):
print("Received SIGTERM — shutting down cleanly.")
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)SIGKILL cannot be caught or handled on Linux or macOS — it terminates the process
immediately. On Windows, SIGKILL does not exist; os.kill(pid, signal.SIGTERM)
sends a WM_CLOSE message instead, and the process can choose to ignore it.
If your tool manages subprocesses and needs to terminate them, use
subprocess.Popen.terminate() (sends SIGTERM on Unix, WM_CLOSE on Windows)
and subprocess.Popen.kill() (sends SIGKILL on Unix, TerminateProcess on
Windows). These abstractions do the right thing on each platform.
Detecting the platform
Avoid platform checks unless the difference is genuinely unavoidable. When you
do need one, sys.platform is the standard way:
import sys
if sys.platform == "win32":
# Windows (32-bit and 64-bit)
config_ext = ".ini"
elif sys.platform == "darwin":
# macOS
config_ext = ".toml"
else:
# Linux and other Unix-like systems
config_ext = ".toml"sys.platform returns "win32" on all Windows Python builds regardless of
architecture. "darwin" is macOS. Anything else is a Unix-like system.
For more fine-grained detection, platform.system() returns "Windows",
"Darwin", or "Linux". Use whichever reads more clearly in context.
The presence of a sys.platform check in your code is a code smell worth
examining. Ask: can the behaviour difference be abstracted behind pathlib,
platformdirs, or subprocess's cross-platform methods? If yes, use the
abstraction and delete the check.
Where to go next
Next: shebang and executables — how #!/usr/bin/env python3 works on Unix,
why it is ignored on Windows, and how pip's entry point launchers bridge the gap.