Python print() — Print Buffering Drops Docker Output
Python’s 8KB block buffer in Docker containers drops print output for 30 minutes — fix with PYTHONUNBUFFERED or flush=True to prevent false deadlocks.
- print() converts values to strings and writes them to stdout, which is buffered by the OS — not displayed immediately
- Five parameters: *objects (values), sep (between values), end (after last value), file (output destination), flush (force write)
- flush=True is non-negotiable in Docker, piped, or containerised environments — without it, output may never appear before a crash
- f-strings are faster than .format() and fail at parse time — always prefer them for embedded variable formatting
- print() writes to stdout by default — errors belong on stderr via file=sys.stderr to keep piped data clean
- Biggest mistake: using print() as production logging — it has no timestamps, no severity levels, no way to disable output without code changes
Every Python developer has done it: spent 45 minutes convinced there's a logic bug, only to realise print() was buffering output and they were reading stale data the entire time. Not a beginner mistake — I've watched a senior engineer waste a morning on this during a live incident because stdout was line-buffered inside a Docker container and nothing was flushing to the log aggregator.
print() looks like the simplest thing in Python. One function, one job — show something on screen. But it has five parameters most beginners never touch, a buffering model that silently eats your debug output at the worst possible moment, and formatting options that, once you know them, make you wonder how you ever lived without them. The gap between knowing print() exists and actually knowing how it works is wider than it looks.
By the end of this article you'll be able to use every parameter print() accepts, format output cleanly using f-strings and the sep and end arguments, write output to files instead of the terminal, force-flush buffered output so it actually appears when you need it, and spot the exact mistakes that cause beginners to think their code isn't running when it's running just fine.
What print() Actually Does — and the Buffering Trap Nobody Warns You About
Before you can use print() well, you need a mental model of what happens when you call it. You type print('hello') and a word appears on screen. Simple. But there are three invisible steps between those two events: Python converts your value to a string, hands it to the operating system's standard output stream (stdout), and the OS decides when to actually display it. That last step is the one that bites people.
Stdout is buffered. That means Python doesn't necessarily write your output to the screen the instant you call print(). It stacks up output in memory and flushes it in chunks — usually when the buffer fills up, when the program ends cleanly, or when a newline character is written. In an interactive terminal, Python uses line-buffering, so you see output after each newline. But pipe that program's output to a file, run it inside Docker, or wrap it in a subprocess, and you get full block-buffering. Your print() calls appear to do nothing until the program exits.
I've seen this burn people specifically in long-running data pipeline scripts — a developer adds print() calls for progress reporting, runs the script piped to tee to capture logs, and sees nothing for 30 minutes then gets a wall of text all at once when the script finishes. The fix is the flush parameter, which we'll get to. But knowing this buffering model exists is the first thing you need, because without it the symptom looks exactly like a hung process.
The Full print() Syntax: All Five Parameters, Actually Explained
The full signature of print() is: print(*objects, sep=' ', end=' ', file=sys.stdout, flush=False). Most tutorials show you the first argument and quietly ignore the other four. That's a mistake, because those four parameters are where print() becomes genuinely useful.
*objects means you can pass as many values as you want, separated by commas. Python converts each one to a string using str() before printing. sep is what gets placed between those values — it defaults to a single space, but you can make it anything: a pipe character, a tab, a comma, an empty string. end is what gets appended after the last value — it defaults to a newline character, which is why each print() call appears on its own line. Change it to an empty string and you can print multiple things on one line across multiple calls.
file lets you redirect print() output to any object that has a write() method — a file handle, sys.stderr, a StringIO buffer, or any custom stream. This is genuinely useful for writing lightweight scripts that log to a file without importing the logging module. flush we already covered in depth: it bypasses the OS buffer and writes to the stream immediately. None of these parameters are optional knowledge — they come up the moment you write anything beyond a simple script.
Formatting Output with f-strings: The Right Tool for the Job
You will format strings inside print() constantly. There are three ways to do it in modern Python: concatenation with +, the old .format() method, and f-strings. Don't use concatenation for anything beyond gluing two things together — it breaks the moment you mix strings and non-strings and it's unreadable at scale. Don't use .format() for new code — it was the right answer before Python 3.6 and it's been outdated since. Use f-strings.
An f-string is a string literal prefixed with f. Any expression inside curly braces gets evaluated and inserted. You can put arithmetic, function calls, method calls, conditional expressions — any valid Python expression — directly inside the braces. You can also add format specifiers after a colon inside the braces to control number formatting, padding, and alignment.
The format specifiers are where beginners usually stop reading, which is a shame because they solve real problems. :.2f gives you a float rounded to two decimal places — essential for money. :, adds thousands separators to integers. :>10 right-aligns a value in a 10-character-wide column. :<10 left-aligns. :^10 centres. These turn chaotic number output into readable, aligned tables without reaching for any external library. The combination :>15,.2f means right-align in a 15-character field, add comma separators, show two decimal places — one specifier replaces what used to take three or four string operations.
Printing Multiple Values, Special Characters, and When to Stop Using print()
There are two small mechanics beginners stumble over: printing multiple values in one call, and dealing with special characters. Passing multiple arguments to print() is cleaner than concatenation — print(first_name, last_name) just works, and you control the separator with sep. You don't need to manually add spaces or call str() on each value — print() handles the conversion internally.
Special characters use escape sequences inside strings. is a newline, \t is a tab, \\ is a literal backslash, and \' or \" escape quotes inside a string. You'll use constantly. You'll use \t occasionally for quick-and-dirty alignment, though f-string width specifiers are cleaner and more predictable for anything that needs to stay aligned across different-length values.
Now — the opinion you didn't ask for but need to hear. print() is a debugging and light-output tool. The moment you're writing a production service, a daemon, a web application, or anything that runs unattended, switch to Python's logging module. It gives you log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL), timestamps, filenames, line numbers, and configurable output destinations — all things print() cannot give you. I've inherited codebases where the entire observability strategy was print() calls sprinkled across 15 files, and debugging a production issue in that codebase meant grepping through 200 identical-looking lines with no timestamps and no severity context. The on-call engineer has no idea which print() is relevant to the current incident and which is leftover from someone's debugging session six months ago. Don't build that codebase. Use print() to learn, use it in one-off scripts, use it for deliberate user-facing terminal output. For everything else: logging.
| Feature / Aspect | print() | logging module |
|---|---|---|
| Setup required | None — works immediately | ~5 lines to configure basicConfig() once at startup |
| Timestamps | No — you add them manually via f-string if you need them | Automatic via %(asctime)s format specifier |
| Log levels (DEBUG/INFO/WARNING/ERROR) | No — all output looks identical regardless of severity | Yes — filter by severity at runtime with no code changes |
| Output destination | stdout or a file via file= parameter | Multiple handlers: file, console, rotating file, network socket |
| Line numbers and filenames | No — you'd have to add them manually | Yes — automatic via %(filename)s and %(lineno)d in the format string |
| Disable output without code changes | No — must delete or comment out calls | Yes — set log level to WARNING at runtime, debug calls disappear |
| Performance in tight loops | Fast — minimal overhead per call | Slightly more overhead per call, negligible in practice outside of extremely tight inner loops |
| Right tool for user-facing terminal output | Yes — clean and appropriate | Technically works but overkill for simple terminal feedback |
| Right tool for production services | No — unstructured, no severity, no aggregator support | Yes — built for exactly this purpose |
| flush= support | Yes — built-in parameter on every call | Configurable per handler via StreamHandler with flush support |
Key Takeaways
- flush=True is not optional in containerised or piped environments — without it, your
print()calls are buffered and may never appear if the process crashes before the buffer drains. Add PYTHONUNBUFFERED=1 to every Dockerfile as a default, not as a reactive fix. - print() writes to stdout by default — errors belong on stderr via file=sys.stderr, or the moment you pipe your program's output anywhere, error messages corrupt the data stream silently and the downstream tool fails with a confusing error that points nowhere near the real problem.
- Reach for f-strings over concatenation or .format() every time — they're faster, they fail at parse time instead of runtime, and format specifiers like :.2f and :>10 replace entire formatting libraries for simple output.
- print() is a learning and light-scripting tool — the moment your code runs unattended, handles real users, or needs timestamps and severity levels, you've already outgrown it and should be using the logging module. The refactor costs five times as much when you do it after an incident.
Common Mistakes to Avoid
- Printing inside a loop expecting real-time output in Docker
Symptom: Output appears all at once when the container exits instead of appearing line-by-line during execution. The on-call engineer thinks the script is hung when it's actually running fine and has been running fine for 25 minutes.
Fix: Add flush=True to everyprint()call inside the loop, or set PYTHONUNBUFFERED=1 in your container's environment variables. For Dockerfiles, add ENV PYTHONUNBUFFERED=1 as a standard practice in your base image so no individual developer has to remember it. - Concatenating a string and an integer directly
Symptom: print('Total: ' + 42) raises TypeError: can only concatenate str (not 'int') to str. The script crashes on a line that looks trivially correct, which is especially confusing for developers coming from JavaScript where + coerces types silently.
Fix: Use an f-string instead: print(f'Total: {42}') — Python handles the conversion automatically. Or pass as separate arguments: print('Total:', 42) and let sep handle the spacing. Never use + to concatenate strings with non-string types. - Printing error messages to stdout instead of stderr
Symptom: Downstream tools piping your stdout get error text mixed into data, silently corrupting output. The next command in the pipeline receives a mix of valid data and error strings and fails with a confusing, unrelated error message that points nowhere near the real problem.
Fix: Use print('Error: something failed', file=sys.stderr) for all error output. This keeps stdout clean for data and stderr clean for diagnostics — the Unix convention that every tool from curl to grep follows. - Using end='' to suppress newlines but forgetting to reset
Symptom: The cursor stays on the same line after the inline sequence finishes — the next print() appends to the same line unexpectedly, producing garbled output like 'Loading...Done!Error: connection timeout' all on one line with no separators.
Fix: Always add an explicitprint()with no arguments (which prints just a newline) when you're done with the inline sequence. This resets the cursor to a new line and restores normalprint()behaviour for subsequent calls. - Formatting floats as currency without a format specifier
Symptom: print(f'Total: £{149.9}') outputs 'Total: £149.9' instead of 'Total: £149.90' — the missing trailing zero looks unprofessional in any user-facing output and outright wrong in financial reporting where two decimal places are a hard requirement.
Fix: Always use :.2f for monetary values: print(f'Total: £{149.9:.2f}'). This guarantees exactly 2 decimal places regardless of the input value, including values that are already round numbers like 150.0.
Interview Questions on This Topic
- QPython's
print()uses stdout buffering. Walk me through exactly what happens to output when a Python script is run inside a Docker container piped to a log aggregator — and what's the specific fix to guarantee everyprint()call appears in the logs before a crash?Mid-levelReveal - QYou're writing a CLI tool that both produces machine-readable output for piping and user-facing status messages. How would you structure your use of
print()and sys.stderr to make the tool composable with standard Unix pipelines?Mid-levelReveal - QWhat's the difference between print(a, b, c) and print(str(a) + str(b) + str(c)) in terms of output, performance, and what happens when one of the values is None?JuniorReveal
Frequently Asked Questions
How do I print without a newline in Python?
Set end='' in your print() call: print('Loading...', end=''). By default, print() appends a newline character after every call — changing end to an empty string suppresses it. When you're ready to move to the next line, call print() with no arguments, which prints just a newline and resets the cursor position.
What's the difference between print() and f-strings in Python?
They're not alternatives — they work together. An f-string is how you format a value into a string; print() is how you display it. You almost always use both: print(f'Total: £{amount:.2f}'). Use an f-string whenever you need to embed a variable or expression into output text with specific formatting; use print() whenever you need to display something to the terminal or write it to a stream.
How do I print to a file in Python using print()?
Pass a file object to the file= parameter: open the file with open('output.txt', 'a') as f, then call print('your text', file=f). Add flush=True if you need the write to happen immediately rather than waiting for the file buffer to fill — important if you're writing progress lines during a long operation. This works for any object with a write() method, not just files — including sys.stderr and StringIO buffers.
Why is my print() output not showing up in Docker logs until the container exits?
Python uses block-buffering when stdout is not connected to a terminal — inside Docker, it never is. The buffer fills and flushes in chunks, and if your process exits before the buffer drains (especially on a crash or SIGKILL), you lose that output entirely. Set the environment variable PYTHONUNBUFFERED=1 in your Dockerfile or docker-compose.yml to force unbuffered stdout globally. For individual critical calls, add flush=True directly to the print() call as an extra guarantee.
That's Python Basics. Mark it forged?
5 min read · try the examples if you haven't