Code of the Day
AdvancedCross-Platform

Platform-specific behaviour

Case sensitivity, line endings, signal handling, and how to detect the current OS when conditional logic is unavoidable.

UtilitiesAdvanced6 min read
Recommended first
By the end of this lesson you will be able to:
  • 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.

Finished reading? Mark it complete to track your progress.

On this page