Homeβ€Ί Pythonβ€Ί Python print() Function: Syntax, Formatting and Real Examples

Python print() Function: Syntax, Formatting and Real Examples

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Python Basics β†’ Topic 14 of 15
Python print() function explained from first principles β€” syntax, sep, end, file, flush parameters, f-strings, and the gotchas that bite beginners in production.
πŸ§‘β€πŸ’» Beginner-friendly β€” no prior Python experience needed
In this tutorial, you'll learn:
  • 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.
  • 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.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
Think of print() as a PA system announcement in a big office building. Your Python program is doing work quietly in a back room, and print() is the moment someone walks to the microphone and broadcasts what's happening to everyone in the building. Without it, work still gets done β€” but nobody outside that room has any idea what's going on. The PA system doesn't change the work; it just makes it visible. That's all print() does β€” it makes your program's internal state visible to you.

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.

BufferingDemo.py Β· PYTHON
1234567891011121314151617181920212223242526272829303132
# io.thecodeforge β€” Python tutorial

import time
import sys

# Simulates a long-running data import job.
# Without flush=True, none of these progress lines appear
# until the entire job finishes β€” which is useless for monitoring.

def run_import_job(total_records: int) -> None:
    print(f"Starting import of {total_records} records...")

    for batch_number in range(1, total_records + 1):
        # Simulate work being done on each record
        time.sleep(0.1)

        # flush=True forces Python to push this line to stdout RIGHT NOW
        # instead of waiting for the buffer to fill.
        # Without this, in a piped or containerised environment,
        # you'd see nothing until the program exits.
        print(
            f"Processed record {batch_number}/{total_records}",
            flush=True  # <-- this is the line that makes monitoring actually work
        )

    # end='' replaces the default newline with nothing,
    # so the next print() continues on the same line.
    print("\nImport complete.", flush=True)


if __name__ == "__main__":
    run_import_job(total_records=5)
β–Ά Output
Starting import of 5 records...
Processed record 1/5
Processed record 2/5
Processed record 3/5
Processed record 4/5
Processed record 5/5

Import complete.
⚠️
Production Trap: Silent Buffering in DockerRunning Python inside Docker with output piped to a log driver? Add PYTHONUNBUFFERED=1 to your environment variables (or run python with the -u flag). Without it, your container logs stay empty until the process dies β€” and if it crashes, you lose every print() call that never flushed. This has killed post-mortem debugging on more incidents than I can count.

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. 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, a log stream, stderr. This is genuinely useful for writing lightweight scripts that log to a file without importing the logging module. flush we already covered. None of these parameters are optional knowledge β€” they come up the moment you write anything beyond a simple script.

CheckoutReceiptPrinter.py Β· PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
# io.thecodeforge β€” Python tutorial

import sys
from datetime import datetime

# Imagine this is a checkout service generating a transaction receipt.
# Each parameter of print() is doing real work here.

def print_receipt(
    order_id: str,
    items: list[tuple[str, float]],
    log_file_path: str
) -> None:

    # Open a file to log receipts β€” print() will write there instead of stdout
    with open(log_file_path, "a") as receipt_log:

        # sep='\n  ' puts each argument on a new indented line.
        # end='' prevents an extra blank line at the start.
        print(
            "=" * 40,
            f"ORDER ID : {order_id}",
            f"TIMESTAMP: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            sep="\n",   # each argument printed on its own line
            end="\n",  # normal newline after the last item
            file=receipt_log,  # writes to the file, not the terminal
            flush=True  # flush immediately so log isn't lost if service crashes
        )

        # Print each line item β€” sep is a pipe character for readability
        for item_name, item_price in items:
            print(
                f"  {item_name}",
                f"Β£{item_price:.2f}",  # :.2f formats float to 2 decimal places
                sep=" | ",  # separator between item name and price
                file=receipt_log,
                flush=True
            )

        # Calculate and print total
        total = sum(price for _, price in items)
        print(f"\nTOTAL: Β£{total:.2f}", file=receipt_log, flush=True)
        print("=" * 40, file=receipt_log, flush=True)

    # Confirm to the terminal (stdout) β€” separate from the file log
    print(f"Receipt for order {order_id} written to {log_file_path}")


if __name__ == "__main__":
    cart = [
        ("Wireless Headphones", 79.99),
        ("USB-C Cable", 12.49),
        ("Screen Protector", 8.00),
    ]
    print_receipt(
        order_id="ORD-20240812-7743",
        items=cart,
        log_file_path="receipts.log"
    )
β–Ά Output
Receipt for order ORD-20240812-7743 written to receipts.log

[Contents of receipts.log:]
========================================
ORD-20240812-7743
2024-08-12 14:33:01
Wireless Headphones | Β£79.99
USB-C Cable | Β£12.49
Screen Protector | Β£8.00

TOTAL: Β£100.48
========================================
⚠️
Senior Shortcut: Print Errors to stderr, Not stdoutUse print('Something went wrong', file=sys.stderr) for error messages. stdout and stderr are separate streams β€” keeping them separate means scripts that pipe your output to another command don't get error text mixed into the data. It's a one-word change that makes your scripts composable with standard Unix tooling.

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.

SalesReportFormatter.py Β· PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
# io.thecodeforge β€” Python tutorial

# Realistic scenario: a sales reporting script that prints
# a formatted summary table to the terminal each morning.
# This is the kind of internal tooling that lives in every company
# and is almost always badly formatted because nobody knew f-string specifiers.

from datetime import date

def print_sales_report(
    report_date: date,
    regional_sales: list[tuple[str, int, float]]
) -> None:

    # Header with dynamic date β€” !s calls str() on the date object
    print(f"\n{'DAILY SALES REPORT':^50}")
    print(f"{'Date: ' + str(report_date):^50}")
    print("-" * 50)

    # Column headers β€” :<20 left-aligns in a 20-char wide column
    # :>10 right-aligns in a 10-char column
    print(f"{'Region':<20} {'Units':>10} {'Revenue':>15}")
    print("-" * 50)

    total_units = 0
    total_revenue = 0.0

    for region_name, units_sold, revenue in regional_sales:
        total_units += units_sold
        total_revenue += revenue

        # :, adds thousands separators: 12000 becomes 12,000
        # :.2f formats to 2 decimal places for currency
        # The full :>15,.2f means: right-align in 15 chars, comma separator, 2 decimal places
        print(
            f"{region_name:<20} "
            f"{units_sold:>10,} "
            f"Β£{revenue:>14,.2f}"
        )

    print("=" * 50)

    # Conditional expression inside f-string β€” shows YoY status inline
    performance_flag = "βœ“ On Target" if total_revenue >= 50_000 else "⚠ Below Target"
    print(f"{'TOTAL':<20} {total_units:>10,} Β£{total_revenue:>14,.2f}")
    print(f"\nStatus: {performance_flag}")


if __name__ == "__main__":
    sales_data = [
        ("North",   4210,  18_430.50),
        ("South",   3875,  15_200.00),
        ("East",    2990,  12_750.75),
        ("West",    5100,  21_880.25),
    ]
    print_sales_report(
        report_date=date(2024, 8, 12),
        regional_sales=sales_data
    )
β–Ά Output

DAILY SALES REPORT
Date: 2024-08-12
--------------------------------------------------
Region Units Revenue
--------------------------------------------------
North 4,210 Β£18,430.50
South 3,875 Β£15,200.00
East 2,990 Β£12,750.75
West 5,100 Β£21,880.25
==================================================
TOTAL 16,175 Β£68,261.50

Status: βœ“ On Target
πŸ”₯
Interview Gold: f-strings vs .format() Performancef-strings are faster than .format() β€” they're evaluated at parse time rather than being dispatched through a method call at runtime. In a tight loop printing thousands of lines (logging, report generation), this difference is measurable. More importantly, f-strings fail at parse time if the variable doesn't exist β€” .format() with a missing key throws a KeyError at runtime, which is a worse time to find out.

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() β€” print() handles the conversion.

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 for anything real.

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 everywhere, and debugging a production issue in that codebase meant guessing which of 200 print() calls was relevant. Don't build that codebase. Use print() to learn, use it in one-off scripts, use it for user-facing terminal output. For everything else: logging.

UserAuthDebugger.py Β· PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
# io.thecodeforge β€” Python tutorial

# Shows the difference between print() for user-facing terminal output
# and the point where you should switch to the logging module.
# This is a simplified auth flow for illustration.

import logging
import sys

# Configure logging β€” this is the pattern you use instead of print()
# for anything that runs in the background or in production.
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d β€” %(message)s",
    stream=sys.stdout  # you can also point this at a file
)

logger = logging.getLogger(__name__)


def attempt_login(username: str, provided_password: str) -> bool:
    """Simulates a login check with both print() and logging to show the contrast."""

    # print() is fine here β€” this is deliberate user-facing terminal output
    print(f"\nAttempting login for: {username}")

    # logging.debug is the right call for internals β€”
    # it includes timestamp, file, line number automatically
    logger.debug("Login attempt started for user: %s", username)

    # Simulated password check (never store plain passwords β€” this is just illustration)
    stored_password_hash = "hashed_secret_123"  # placeholder
    password_matches = provided_password == "correct_password"  # simplified check

    if not password_matches:
        # logging.warning is appropriate β€” it's an event worth recording
        logger.warning("Failed login attempt for user: %s", username)

        # print() for the user-facing message β€” they need to see this
        print("Login failed. Check your credentials.")
        return False

    logger.info("Successful login for user: %s", username)
    print(f"Welcome back, {username}!")
    return True


if __name__ == "__main__":
    # Demonstrates: multiple values in one print(), sep, and special characters
    print("=" * 40)
    print("Username:", "Status:", "Result:", sep="\t")  # tab-separated header
    print("=" * 40)

    attempt_login(username="alice", provided_password="wrong_password")
    print("-" * 40)  # \n is already the default end= so no need to add it
    attempt_login(username="alice", provided_password="correct_password")
β–Ά Output
========================================
Username: Status: Result:
========================================

Attempting login for: alice
2024-08-12 14:33:01,234 [DEBUG] UserAuthDebugger.py:30 β€” Login attempt started for user: alice
2024-08-12 14:33:01,235 [WARNING] UserAuthDebugger.py:39 β€” Failed login attempt for user: alice
Login failed. Check your credentials.
----------------------------------------

Attempting login for: alice
2024-08-12 14:33:01,236 [DEBUG] UserAuthDebugger.py:30 β€” Login attempt started for user: alice
2024-08-12 14:33:01,237 [INFO] UserAuthDebugger.py:43 β€” Successful login for user: alice
Welcome back, alice!
⚠️
Never Do This: print() as Your Production LoggerI've seen a microservice with 400 print() statements as its only observability layer. When it started misbehaving at 2am, the on-call engineer had zero timestamps, zero severity levels, and zero context on which calls were related to which request. The fix was a week-long refactor to replace every print() with logging calls. Switch to logging the moment your script runs unattended or handles real users.
Feature / Aspectprint()logging module
Setup requiredNone β€” works immediately~5 lines to configure basicConfig()
TimestampsNo β€” you add them manually via f-stringAutomatic via %(asctime)s format
Log levels (DEBUG/INFO/WARNING/ERROR)No β€” all output looks identicalYes β€” filter by severity at runtime
Output destinationstdout or a file via file= parameterMultiple handlers: file, console, network
Line numbers and filenamesNoYes β€” automatic via format string
Disable output without code changesNo β€” must delete or comment out callsYes β€” set log level to WARNING at runtime
Performance in tight loopsFast β€” minimal overheadSlightly more overhead but negligible in practice
Right tool for user-facing terminal outputYesTechnically works but overkill
Right tool for production servicesNoYes β€” this is what it's built for
flush= supportYes β€” built-in parameterConfigurable via handler StreamHandler(flush)

🎯 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.
  • 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.
  • 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.

⚠ Common Mistakes to Avoid

  • βœ•Mistake 1: Printing inside a loop expecting real-time output in a Docker container β€” output appears all at once when the script ends β€” add flush=True to every print() call inside the loop, or set PYTHONUNBUFFERED=1 in your container's environment variables.
  • βœ•Mistake 2: Concatenating a string and an integer directly β€” print('Total: ' + 42) raises TypeError: can only concatenate str (not 'int') to str β€” use an f-string instead: print(f'Total: {42}') or pass as separate arguments: print('Total:', 42).
  • βœ•Mistake 3: Using print() for error messages that go to stdout instead of stderr β€” downstream tools piping your stdout get error text mixed into data, silently corrupting output β€” use print('Error: something failed', file=sys.stderr) to keep error output on the correct stream.
  • βœ•Mistake 4: Using end='' to suppress newlines but forgetting that the cursor stays on the same line forever β€” the next print() appends to the same line unexpectedly β€” always add an explicit print() with no arguments (which prints just a newline) when you're done with the inline sequence.
  • βœ•Mistake 5: Formatting floats as currency without a format specifier β€” print(f'Total: Β£{149.9}') outputs Total: Β£149.9 instead of Total: Β£149.90 β€” always use :.2f for monetary values: print(f'Total: Β£{149.9:.2f}').

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 every print() call appears in the logs before a crash?
  • 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?
  • 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?

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.

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; use print() whenever you need to display something to the terminal or write it to a file.

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. This works for any object with a write() method, not just files β€” including sys.stderr.

Why is my print() output not showing up in Docker logs until the container exits?

Python uses block-buffering when stdout is not a terminal β€” inside Docker, it isn't. The buffer fills and flushes in chunks, and if your process exits before the buffer drains (especially on a crash), you lose that output entirely. Set the environment variable PYTHONUNBUFFERED=1 in your Dockerfile or docker-compose.yml, or run Python with python -u. For individual critical calls, add flush=True directly to the print() call.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousPython append(): Add Items to a List (with Examples)Next β†’Python range() Function Explained with Examples
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged