Input and Output - Engineering Python I/O from stdin to Format Specs
Reading time: ~25 minutes | Level: Foundation → Engineering
Consider this code, written by a developer who has been using Python for a year:
import subprocess
proc = subprocess.Popen(["python", "worker.py"], stdout=subprocess.PIPE)
for line in proc.stdout:
print(line.decode())
The worker script does this:
import time
for i in range(10):
print(f"Processing item {i}")
time.sleep(1)
The developer expects to see each line appear as the worker produces it. Instead, nothing appears for ten seconds, and then all ten lines print simultaneously. The bug is not in the logic - it is in buffering. Understanding why this happens, and how to fix it, requires understanding Python I/O at a level that goes far beyond print() and input().
What You Will Learn
By the end of this lesson you will be able to explain exactly what print() does under the hood, control stdout buffering to fix real subprocess and CI pipeline bugs, use f-strings and the format spec mini-language to produce precisely formatted output, handle stdin correctly in scripts including EOF conditions, write to stderr the right way for CLI tools, and avoid the five most common I/O pitfalls that trip up working developers.
Prerequisites
- Variables, data types, and basic control flow in Python
- A passing familiarity with the terminal and running Python scripts
- No prior knowledge of streams or buffering is required
The Three Standard Streams
Before touching print(), you need a mental model of where output actually goes. Every Unix-like process - and every Python script - starts with three open file descriptors handed to it by the operating system:
In Python, these are exposed as sys.stdin, sys.stdout, and sys.stderr. They are not magic - they are file-like objects with .read(), .write(), .flush(), and .fileno() methods.
import sys
print(type(sys.stdout)) # <class '_io.TextIOWrapper'>
print(sys.stdout.fileno()) # 1
print(sys.stdout.encoding) # utf-8 (on most modern systems)
print(sys.stdout.line_buffering) # True if connected to a terminal
The distinction between stdout and stderr is architectural: stdout carries program output - data that other processes or users will consume. Stderr carries diagnostics - messages intended for the operator running the script. When you pipe a program's output through grep, stderr still appears on your terminal even though stdout does not, because they are separate streams.
print() Anatomy - Every Parameter Explained
Most developers use print() with a single argument. The full signature is:
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
Each parameter solves a specific engineering problem.
sep - The Separator Between Objects
print("2026", "03", "01", sep="-") # 2026-03-01
print("host", "port", sep=":") # host:port
print(10, 20, 30, sep=" | ") # 10 | 20 | 30
sep is applied between each positional argument. It defaults to a single space. Pass sep="" to join with no separator, which is sometimes cleaner than string concatenation when printing multiple pieces:
user = "alice"
domain = "example.com"
end - What Prints After All Objects
By default end='\n', which is why each print() call starts on a new line. Override it to control exactly where the cursor goes:
# Progress indicator on a single line
import time
for i in range(5):
print(f"\rProcessing: {i+1}/5", end="", flush=True)
time.sleep(0.3)
print() # Move to next line when done
end="" is also used in log-line construction where you want precise control over newlines, or when you are building table rows character by character.
file - Redirecting Output to Any File-Like Object
print() writes to sys.stdout by default, but file accepts any object with a .write() method. The most important use is printing to stderr:
import sys
def warn(message: str) -> None:
print(f"WARNING: {message}", file=sys.stderr)
def error_and_exit(message: str, code: int = 1) -> None:
print(f"ERROR: {message}", file=sys.stderr)
sys.exit(code)
You can also print directly to an open file handle:
with open("audit.log", "a") as log:
print(f"[2026-03-01] User login: alice", file=log)
This is identical to calling log.write(f"...\n") - print() handles the str() conversion and the end newline for you.
flush - Forcing Immediate Write
This is the parameter that resolves the subprocess buffering bug from the introduction.
print("Processing item 3", flush=True)
When flush=True, Python calls sys.stdout.flush() immediately after writing. Without it, output may sit in a buffer for hundreds of milliseconds or until the buffer fills - invisible to any process reading that stream.
stdout Buffering - The Mechanism Behind the Mystery
Python's stdout operates in one of three buffering modes:
| Mode | When active | Behavior |
|---|---|---|
| Unbuffered | Binary mode | Every write() goes immediately to the OS |
| Line-buffered | TTY / terminal | Flush automatically on every \n - why REPL output appears instantly |
| Block-buffered | Pipe / file | Accumulate data in a 4 KB or 8 KB buffer; flush when full or at exit |
When Python detects that stdout is connected to a terminal (a TTY), it uses line-buffering: any \n character triggers a flush. This is why interactive Python feels instant. But when stdout is redirected to a pipe - as happens in subprocess.Popen with stdout=PIPE, in CI pipelines, in Docker container logs, and in cron jobs - Python switches to block-buffering. Output accumulates until the buffer fills (typically 8 KB) or the process exits.
Fixing the Subprocess Buffering Problem
Three approaches, in order of preference:
Option 1: Use flush=True in the worker script
# worker.py - fix at the source
import time
for i in range(10):
print(f"Processing item {i}", flush=True)
time.sleep(1)
Option 2: Run Python with -u (unbuffered) flag
python -u worker.py
The -u flag forces unbuffered binary mode for stdin, stdout, and stderr. You can also set the PYTHONUNBUFFERED environment variable to any non-empty string for the same effect - this is the standard approach in Docker:
ENV PYTHONUNBUFFERED=1
Option 3: Use sys.stdout.reconfigure() inside the script
import sys
sys.stdout.reconfigure(line_buffering=True)
:::warning Buffering in CI Pipelines
Many developers are confused when their progress bars and status messages appear in a giant batch at the end of a CI job log. The CI runner captures stdout through a pipe, triggering block-buffering. Always set PYTHONUNBUFFERED=1 in your CI environment variables or use flush=True in loops that produce progress output.
:::
input() Internals - What Actually Happens
name = input("Enter your name: ")
Under the hood, input() does three things in sequence:
- Writes the prompt string to
sys.stdout(without a newline), flushing stdout so the prompt appears before blocking - Reads a single line from
sys.stdin- blocking the current thread until the user presses Enter - Strips the trailing newline from the returned string
The return value is always str. Always. No exceptions for "obviously numeric" input.
x = input("Enter a number: ")
print(type(x)) # <class 'str'> - even if the user typed "42"
What Happens at EOF
When stdin reaches end-of-file - which happens when the user presses Ctrl+D (Unix) or Ctrl+Z (Windows), or when a script pipes empty input - input() raises EOFError:
try:
value = input("Enter value: ")
except EOFError:
print("\nNo input provided - using default", file=sys.stderr)
value = "default"
This matters for scripts designed to read from pipes or be used in automation. Always handle EOFError in production CLI tools.
Input Validation Pattern
Never trust raw input. The canonical validation loop:
def get_positive_int(prompt: str) -> int:
"""Read and validate a positive integer from stdin."""
while True:
raw = input(prompt).strip()
try:
value = int(raw)
if value <= 0:
raise ValueError("Must be positive")
return value
except ValueError as exc:
print(f"Invalid input: {exc}. Please try again.", file=sys.stderr)
The .strip() call removes accidental whitespace. The ValueError handler catches both int() conversion failure and the explicit domain check. Notice that the error message goes to stderr - the prompt and the valid result go to stdout - keeping streams semantically clean.
:::danger eval(input()) is Unsafe
eval(input("Enter expression: ")) executes arbitrary Python code. If you need to evaluate mathematical expressions from users, use ast.literal_eval() for simple literals or a proper expression parser. Never use eval() on untrusted input in any environment.
:::
f-Strings - The Full Format Specification
f-strings (introduced in Python 3.6) are syntactic sugar over the format spec mini-language, but they are fast - the expression inside {} is evaluated at runtime with no string parsing overhead.
Basic Expression Evaluation
name = "Alice"
score = 97.5
rank = 1
print(f"Player: {name}") # Player: Alice
print(f"Score: {score}") # Score: 97.5
print(f"Rank: {rank}") # Rank: 1
print(f"Next rank: {rank + 1}") # Next rank: 2 (arbitrary expression)
print(f"Upper: {name.upper()}") # Upper: ALICE (method call)
print(f"Doubled: {score * 2:.1f}") # Doubled: 195.0 (expression + format spec)
The Format Specification Mini-Language
The format spec lives after the : inside {}. Its full grammar is:
{value:[fill][align][sign][z][#][0][width][grouping][.precision][type]}
fill = any character to pad with
align = < (left) > (right) ^ (center) = (pad after sign)
sign = + (always) - (only negative, default) ' ' (space for positive)
width = minimum field width (integer)
grouping = , (comma) _ (underscore) - for thousand separators
.precision = decimal places for floats, max chars for strings
type = d f e g s b o x X % (see table below)
Alignment and Width
name = "Bob"
print(f"|{name:<10}|") # |Bob | left-aligned in 10-char field
print(f"|{name:>10}|") # | Bob| right-aligned
print(f"|{name:^10}|") # | Bob | centered
print(f"|{name:*^10}|") # |***Bob****| centered, filled with *
This is how you build aligned table output without a library:
headers = ["Name", "Score", "Grade"]
rows = [("Alice", 97.5, "A"), ("Bob", 83.2, "B"), ("Charlie", 91.0, "A-")]
print(f"{'Name':<12} {'Score':>8} {'Grade':^6}")
print("-" * 28)
for name, score, grade in rows:
print(f"{name:<12} {score:>8.1f} {grade:^6}")
Output:
Name Score Grade
----------------------------
Alice 97.5 A
Bob 83.2 B
Charlie 91.0 A-
Number Formatting
n = 1234567.891
print(f"{n:,.2f}") # 1,234,567.89 - comma separator, 2 decimal places
print(f"{n:_.2f}") # 1_234_567.89 - underscore separator (Python 3.6+)
print(f"{n:.2e}") # 1.23e+06 - scientific notation
print(f"{n:.4g}") # 1.235e+06 - general: picks e or f, 4 sig figs
ratio = 0.8735
print(f"{ratio:.1%}") # 87.4% - percentage (multiplies by 100)
n_int = 255
print(f"{n_int:d}") # 255 - decimal integer
print(f"{n_int:b}") # 11111111 - binary
print(f"{n_int:o}") # 377 - octal
print(f"{n_int:x}") # ff - hex lowercase
print(f"{n_int:X}") # FF - hex uppercase
print(f"{n_int:#x}") # 0xff - hex with prefix
print(f"{n_int:08b}") # 11111111 - zero-padded 8-char binary
!r, !s, and !a Conversion Flags
These apply before the format spec:
text = "hello\nworld"
print(f"{text!s}") # hello (str() - newline is rendered)
# world
print(f"{text!r}") # 'hello\nworld' (repr() - shows escape sequences)
print(f"{text!a}") # 'hello\nworld' (ascii() - non-ASCII chars escaped)
!r is invaluable for debugging - you see exactly what is in the string including invisible characters.
Nested f-Strings and Dynamic Format Specs
width = 10
precision = 3
value = 3.14159
# The format spec itself can be an f-string expression
print(f"{value:{width}.{precision}f}") # " 3.142"
This is useful when alignment widths are computed at runtime - for example, sizing a table column to fit the widest value.
:::note f-String Debug Format (Python 3.8+)
The = specifier prints both the variable name and its value - invaluable for debugging:
x = 42
y = x * 2 + 1
print(f"{x=}, {y=}") # x=42, y=85
:::
str.format() and % Formatting
str.format() - Positional and Keyword
# Positional
"Hello, {}! You are {} years old.".format("Alice", 30)
# Indexed
"{0} and {1}, then {0} again".format("first", "second")
# "first and second, then first again"
# Keyword
"{name} scored {score:.1f}".format(name="Bob", score=97.5)
# Using a dict
data = {"name": "Charlie", "city": "London"}
"{name} lives in {city}".format(**data)
str.format() uses the same format spec mini-language as f-strings after the :. It is preferred over f-strings when the template string comes from configuration or a database (since f-strings are evaluated immediately in source code and cannot be stored as data).
% Formatting - Old-Style but Not Dead
"Hello, %s!" % "world"
"Pi is approximately %.4f" % 3.14159
"Name: %s, Age: %d, Score: %.1f" % ("Alice", 30, 97.5)
The % format codes:
| Code | Meaning |
|---|---|
%s | String (calls str()) |
%r | Repr (calls repr()) |
%d | Integer |
%f | Float (6 decimal places by default) |
%.2f | Float with 2 decimal places |
%e | Scientific notation |
%x | Hex integer |
%o | Octal integer |
%05d | Zero-padded integer in 5-char field |
% formatting still matters because the logging module uses it - and for good reason. Logging defers the string interpolation until it is actually needed. If the log level means the message will never be written, no string is constructed:
import logging
logging.basicConfig(level=logging.INFO)
# CORRECT - interpolation is deferred
logging.debug("Processing user %s with payload %r", user_id, payload)
# WRONG - string is built even if DEBUG level is disabled
logging.debug(f"Processing user {user_id} with payload {payload!r}")
:::tip Logging Always Use % Style
Even when you use f-strings everywhere else, use %-style formatting in logging calls. The logging framework handles the interpolation internally, and this gives it the ability to skip expensive string building for suppressed log levels.
:::
Reading from stdin in Scripts
Line-by-Line Reading
When a script is designed to process piped input, read from sys.stdin directly:
cat data.txt | python process.py
# process.py
import sys
for line in sys.stdin:
line = line.rstrip("\n") # strip trailing newline
if line: # skip blank lines
result = line.upper()
print(result)
Iterating over sys.stdin is memory-efficient - it reads one line at a time without loading the entire file.
Reading All of stdin
import sys
content = sys.stdin.read() # reads everything until EOF
lines = sys.stdin.readlines() # reads all lines into a list
Use sys.stdin.read() for small inputs when you need the full content (JSON parsing, for instance). Use the iterator for large or streaming inputs.
EOF Detection
import sys
data = []
try:
while True:
line = sys.stdin.readline()
if not line: # readline() returns '' at EOF (not '\n')
break
data.append(line.rstrip())
except KeyboardInterrupt:
pass # Ctrl+C is also a valid way to signal end of input
The critical distinction: readline() returns '\n' for a blank line but '' at EOF. Do not confuse the two.
pprint - Pretty-Printing Structured Data
For nested dictionaries, lists of objects, and API responses, the standard print() produces an unreadable single-line blob. pprint formats complex structures with indentation:
from pprint import pprint, pformat
data = {
"users": [
{"name": "Alice", "roles": ["admin", "editor"], "score": 97.5},
{"name": "Bob", "roles": ["viewer"], "score": 83.2},
],
"metadata": {"version": "2.1", "region": "us-east-1"},
}
pprint(data)
# {'metadata': {'region': 'us-east-1', 'version': '2.1'},
# 'users': [{'name': 'Alice', 'roles': ['admin', 'editor'], 'score': 97.5},
# {'name': 'Bob', 'roles': ['viewer'], 'score': 83.2}]}
pprint(data, width=40, depth=2) # control line width and max nesting depth
pformat() returns the formatted string instead of printing it - useful for logging:
import logging
logging.info("Response payload:\n%s", pformat(data))
sys.argv - Command-Line Arguments
When a user runs python script.py arg1 arg2, Python stores the arguments in sys.argv:
import sys
# sys.argv[0] is always the script name
script_name = sys.argv[0] # "script.py"
args = sys.argv[1:] # ["arg1", "arg2"]
if len(sys.argv) < 2:
print(f"Usage: {script_name} <filename>", file=sys.stderr)
sys.exit(1)
filename = sys.argv[1]
All entries in sys.argv are strings - just like input(), there is no automatic type conversion. For production CLI tools, use argparse (covered in a later lesson) which handles types, validation, help text, and optional arguments automatically.
Pitfalls Reference
1. input() Always Returns str
# Bug: comparing str to int
age = input("Age: ")
if age > 18: # TypeError: '>' not supported between str and int
print("Adult")
# Fix
age = int(input("Age: "))
2. Print Buffering in Subprocesses
# Bug: output appears in a batch at the end
proc = subprocess.Popen(["python", "worker.py"], stdout=subprocess.PIPE)
# Fix option A: add flush=True in worker.py
# Fix option B: python -u worker.py
# Fix option C: PYTHONUNBUFFERED=1 in environment
3. \n vs os.linesep
import os
# os.linesep is '\r\n' on Windows, '\n' on Unix
# In text mode (the default), Python translates '\n' to os.linesep
# In binary mode, you must handle this yourself
# This is correct for text files - let Python handle the translation
with open("file.txt", "w") as f:
f.write("line one\nline two\n")
4. Unicode in print() on Windows
Windows terminals historically default to code page 1252 or 850, not UTF-8. Printing characters outside the codepage raises UnicodeEncodeError:
import sys
# Check your terminal encoding
print(sys.stdout.encoding) # may be 'cp1252' on Windows
# Force UTF-8 output (Python 3.7+)
sys.stdout.reconfigure(encoding="utf-8")
# Or run Python with: python -X utf8 script.py
# Or set: PYTHONUTF8=1 in environment
5. Printing to stderr Does Not Flush by Default
# Bug: error message may not appear before program exits
print("Fatal error", file=sys.stderr)
sys.exit(1)
# Defensive fix
print("Fatal error", file=sys.stderr, flush=True)
sys.exit(1)
Interview Questions and Answers
Q1: What are the four parameters of print() beyond *objects? Give a practical use case for each.
sep controls the string placed between each positional argument (default ' '). Use it to format CSV-style output: print(*row, sep=","). end controls what is appended after the last argument (default '\n'). Use end="" for in-place progress indicators with \r. file accepts any object with .write() and defaults to sys.stdout. Pass sys.stderr to route error messages to the error stream. flush when True calls .flush() immediately after writing, which is essential when stdout is connected to a pipe and you need real-time output.
Q2: Why does Python use block-buffering when stdout is connected to a pipe, and what are two ways to override it?
When stdout is a terminal (TTY), Python detects this via sys.stdout.isatty() and uses line-buffering so interactive output is responsive. When stdout is a pipe or file, Python uses block-buffering (typically 8 KB blocks) because the assumption is that bulk I/O performance matters more than latency - each write() system call is expensive, so accumulating data reduces call frequency. Override it with python -u (sets the -u unbuffered flag), by setting the PYTHONUNBUFFERED=1 environment variable, or by calling sys.stdout.reconfigure(line_buffering=True) inside the script.
Q3: Explain the f-string format spec f"{value:>10.3f}". What does each part mean?
The > specifies right-alignment. 10 is the minimum field width - the output will be at least 10 characters wide, padded with spaces on the left. .3 is the precision - three digits after the decimal point. f is the type code for fixed-point floating-point notation. So f"{3.14159:>10.3f}" produces " 3.142" - right-aligned in a 10-character field with three decimal places.
Q4: Why does the logging module use %-style formatting instead of f-strings, even in modern Python code?
The logging module defers string interpolation until it is certain the message will actually be written. If you call logging.debug("payload: %r", large_object) but the effective log level is INFO, the repr(large_object) call never happens - no CPU time is spent constructing a string that will be discarded. With f-strings, the expression is evaluated before logging.debug() is even called, so you always pay the cost of building the string. This is a significant performance concern in hot paths.
Q5: What does input() return when it encounters EOF, and why does this matter for scripts that read from pipes?
input() raises EOFError at EOF. When a script is run as part of a pipeline - echo "data" | python process.py - stdin will reach EOF when the upstream process finishes writing. A script that calls input() in a loop without an EOFError handler will crash with an unhandled exception. Always wrap input() in a try/except EOFError block in any script designed for non-interactive use.
Q6: What is the difference between sys.stdin.readline() returning "" versus "\n"?
readline() returns "\n" for a blank line - a line with only a newline character. It returns "" (an empty string) exclusively at end-of-file. This is the canonical way to detect EOF when using readline() in a loop: while True: line = sys.stdin.readline(); if not line: break. Mistakenly treating an empty line as EOF, or vice versa, causes incorrect parsing of text streams.
Graded Practice Challenges
Level 1 - Predict the Output
Without running the code, write exactly what will be printed.
items = [10, 20, 30]
print(*items, sep=" -> ", end=" END\n")
print(f"{'python':*^12}")
print(f"{255:#010b}")
Show Answer
10 -> 20 -> 30 END
***python***
0b11111111
Line 1: *items unpacks the list into three positional arguments; sep=" -> " joins them; end=" END\n" replaces the default newline.
Line 2: ^12 centers "python" in a 12-character field, filled with *. "python" is 6 characters, leaving 6 padding characters split evenly: 3 left, 3 right.
Line 3: # adds the 0b prefix; 010 pads to total width 10 including the prefix; b formats as binary. 255 in binary is 11111111 (8 bits). With the 0b prefix that is 10 characters total: 0b11111111.
Level 2 - Debug the Code
This script is supposed to stream progress output while a subprocess runs, but the output appears all at once at the end. Identify all the bugs and rewrite the relevant parts.
import subprocess
import sys
proc = subprocess.Popen(
["python", "heavy_task.py"],
stdout=subprocess.PIPE
)
for line in proc.stdout:
output = line.decode("utf-8")
print(output)
print("Done")
The heavy_task.py file does:
import time
for i in range(5):
print(f"Step {i+1} complete")
time.sleep(1)
Show Answer
There are two separate buffering problems:
Bug 1: heavy_task.py writes to a pipe (because the parent process captures its stdout), so Python switches to block-buffering. The output sits in the buffer and is only flushed when the buffer fills or the script exits. Fix: add flush=True to the print call inside heavy_task.py, or run it with -u.
Bug 2: In the parent script, print(output) adds a second newline because output already contains the trailing \n from readline(). The output will have double-spaced lines. Fix: strip the line or use end="".
Corrected heavy_task.py:
import time
for i in range(5):
print(f"Step {i+1} complete", flush=True)
time.sleep(1)
Corrected parent script:
import subprocess
proc = subprocess.Popen(
["python", "-u", "heavy_task.py"], # -u as defense in depth
stdout=subprocess.PIPE,
)
for line in proc.stdout:
output = line.decode("utf-8").rstrip("\n")
print(output, flush=True) # flush so our own stdout streams in real time
print("Done")
Level 3 - Design Challenge
Design and implement a function format_table(headers, rows, col_widths=None) that:
- Accepts a list of header strings, a list of row tuples, and an optional list of column widths
- If
col_widthsis not provided, computes the width of each column as the maximum of the header width and the widest value in that column - Left-aligns string values and right-aligns numeric values
- Prints a header row, a separator line made of dashes, and one row per data tuple
- Writes the entire output to an arbitrary file-like object (defaulting to
sys.stdout)
Example call:
format_table(
headers=["Name", "Department", "Salary"],
rows=[
("Alice", "Engineering", 120000),
("Bob", "Marketing", 85000),
("Charlie", "Engineering", 135000),
]
)
Expected output:
Name Department Salary
--------- ----------- --------
Alice Engineering 120000
Bob Marketing 85000
Charlie Engineering 135000
Show Answer
import sys
from typing import Any, IO, List, Optional, Tuple
def format_table(
headers: List[str],
rows: List[Tuple[Any, ...]],
col_widths: Optional[List[int]] = None,
file: IO[str] = sys.stdout,
) -> None:
"""
Print a formatted table with auto-computed or explicit column widths.
String values are left-aligned; numeric values are right-aligned.
The table is written to `file` (default: sys.stdout).
"""
if col_widths is None:
col_widths = []
for i, header in enumerate(headers):
max_data_width = max(
(len(str(row[i])) for row in rows), default=0
)
col_widths.append(max(len(header), max_data_width))
def format_cell(value: Any, width: int) -> str:
if isinstance(value, (int, float)):
return f"{value:>{width}}"
return f"{str(value):<{width}}"
# Header row
header_cells = [f"{h:<{w}}" for h, w in zip(headers, col_widths)]
print(" ".join(header_cells), file=file)
# Separator
separators = ["-" * w for w in col_widths]
print(" ".join(separators), file=file)
# Data rows
for row in rows:
cells = [format_cell(val, width) for val, width in zip(row, col_widths)]
print(" ".join(cells), file=file)
Key design decisions: the file parameter mirrors print()'s own file parameter, making the function composable with any writable stream. The alignment logic branches on type rather than trying to parse strings, which is more robust. Column widths default to None and are computed lazily inside the function so the caller does not need to count characters manually.
Quick Reference Cheatsheet
| Task | Code |
|---|---|
| Print with custom separator | print(a, b, c, sep=", ") |
| Print without newline | print("text", end="") |
| Print to stderr | print("err", file=sys.stderr) |
| Force flush | print("msg", flush=True) |
| Right-align in 10 chars | f"{val:>10}" |
| Left-align in 10 chars | f"{val:<10}" |
| Center in 10 chars | f"{val:^10}" |
| 2 decimal places | f"{val:.2f}" |
| Comma thousands separator | f"{val:,.0f}" |
| Percentage | f"{val:.1%}" |
| Hex with prefix | f"{val:#x}" |
| Binary with prefix | f"{val:#b}" |
| Debug variable name+value | f"{val=}" (Python 3.8+) |
| repr inside f-string | f"{val!r}" |
| Read one line from stdin | line = sys.stdin.readline() |
| Read all of stdin | content = sys.stdin.read() |
| Handle EOF from input() | try: input() except EOFError: ... |
| Pretty-print nested data | from pprint import pprint; pprint(data) |
| Unbuffered subprocess | python -u script.py or PYTHONUNBUFFERED=1 |
| Logging format (deferred) | logging.info("val: %r", val) |
Key Takeaways
print()has four non-obvious parameters -sep,end,file, andflush- each solving a distinct engineering problem; knowing all four makes you a significantly more effective Python programmer.- stdout switches from line-buffering to block-buffering whenever it is connected to a pipe, which causes the "output appears at the end" bug in subprocesses and CI pipelines; fix it with
flush=True,python -u, orPYTHONUNBUFFERED=1. input()always returnsstrand raisesEOFErrorat end-of-file; both facts must be handled explicitly in any production CLI tool.- The f-string format spec mini-language (
:<10,:>10,:.2f,:,,:#x,:.1%) lets you produce precisely aligned, formatted output without any third-party library. - The
loggingmodule deliberately uses%-style formatting to defer string construction; always follow this pattern in logging calls even when you use f-strings everywhere else. - Stdout and stderr are separate streams by design: stdout carries program data, stderr carries diagnostics; mixing them corrupts pipelines and confuses log aggregators.
- On Windows, the terminal may not be UTF-8; use
sys.stdout.reconfigure(encoding="utf-8")or thePYTHONUTF8=1environment variable to avoidUnicodeEncodeErroron international text.
