Intermediate

I/O Operations in Python

Learn console input and output, file reading and writing, formatted output, and I/O error handling in Python with Docker-ready examples

Input and output (I/O) is how a program communicates with the outside world—printing results to the terminal, reading what a user types, and persisting data to files. You already met print() in Hello World; this tutorial goes deeper into formatting that output, reading from standard input, and working with files.

Python’s dynamic, “batteries included” philosophy shows through clearly in I/O. There are no stream classes to instantiate, no manual buffer management, and no file handles to remember to close if you use the idiomatic with statement. Files are ordinary objects, print() and input() are built-in functions, and errors surface as exceptions you can catch. As a multi-paradigm language, Python lets you write I/O in a straightforward procedural style while still leaning on context managers—a small object-oriented convenience that guarantees cleanup.

In this tutorial you’ll learn how to format console output with f-strings and the print() parameters, read typed input and convert it to the right type, write and read files safely with the with statement, and handle common I/O errors gracefully.

Formatted Console Output

print() does far more than emit a fixed string. It accepts multiple arguments, separator and terminator options, and works hand-in-hand with Python’s formatting mini-language.

Create a file named formatted_output.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Formatted console output in Python

name = "Ada"
age = 36

# f-strings (Python 3.6+) are the modern, readable way to interpolate values
print(f"Name: {name}, Age: {age}")

# Number formatting inside f-strings uses a colon and a format spec
pi = 3.14159265
print(f"Pi rounded: {pi:.2f}")
print(f"Zero-padded: {age:05d}")

# The older str.format() method still works and reads the same way
print("Name: {}, Age: {}".format(name, age))

# print() accepts sep= to join its arguments and end= to replace the newline
print("a", "b", "c", sep="-")
print("no newline here", end=" >>> ")
print("same line")

Here {pi:.2f} rounds the float to two decimal places, and {age:05d} pads the integer to a width of five with leading zeros. The sep parameter controls what goes between arguments, while end controls what goes after the last one—setting end="" or another string lets you keep output on the same line.

Reading Input from the User

The built-in input() function reads one line from standard input. It always returns a string, so numeric input must be converted explicitly—a direct consequence of Python’s strong typing, which won’t silently treat "36" as the number 36.

Create a file named read_input.py:

1
2
3
4
5
6
7
8
9
# Reading input from standard input

# input() prints its prompt, then returns whatever the user types (as a string)
name = input("Enter your name: ")
print(f"Hello, {name}!")

# Convert to int() because input() always returns a string
age = int(input("Enter your age: "))
print(f"Next year you will be {age + 1}")

Because these programs wait for input, you’ll typically pipe data into them when running non-interactively (shown in the Docker section below). If you ran this without providing the age as a number—say you typed thirty—the int() call would raise a ValueError, which the next section shows how to handle.

Writing and Reading Files

Files are opened with open(), which returns a file object. The idiomatic pattern is the with statement (a context manager): it guarantees the file is closed automatically when the block ends, even if an error occurs. The mode string selects the operation—"w" writes (creating or overwriting), "a" appends, and "r" reads.

Create a file named file_io.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Writing to and reading from files

# "w" mode creates the file (or overwrites it) and opens it for writing
with open("notes.txt", "w") as f:
    f.write("First line\n")
    f.write("Second line\n")

# "a" mode appends to the end without touching existing content
with open("notes.txt", "a") as f:
    f.write("Third line\n")

# "r" mode reads; .read() returns the entire file as one string
with open("notes.txt", "r") as f:
    content = f.read()
print("Full contents:")
print(content)

# Iterating over a file object yields one line at a time (memory-friendly)
with open("notes.txt", "r") as f:
    for number, line in enumerate(f, start=1):
        print(f"{number}: {line.rstrip()}")

Reading the whole file with .read() is convenient for small files, but iterating line by line (for line in f) is the memory-friendly choice for large ones—it never loads the entire file at once. Here enumerate(f, start=1) numbers each line, and line.rstrip() trims the trailing newline that comes with each line of text.

Handling I/O Errors

I/O is where things go wrong: files go missing, permissions are denied, and user input is malformed. Python surfaces these as exceptions, so the natural way to handle them is try/except. This keeps the happy path readable while still recovering from failure.

Create a file named io_errors.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Handling I/O errors gracefully

# Reading a file that may not exist
try:
    with open("missing.txt", "r") as f:
        data = f.read()
    print(data)
except FileNotFoundError:
    print("Error: the file 'missing.txt' was not found")

# Converting values that may not be valid numbers
raw_values = ["42", "hello", "17"]
for value in raw_values:
    try:
        number = int(value)
        print(f"Parsed {value!r} -> {number}")
    except ValueError:
        print(f"Could not parse {value!r} as an integer")

# OSError is the base class for most file-system failures
try:
    with open("output.txt", "w") as f:
        f.write("Saved successfully\n")
    print("Write complete")
except OSError as e:
    print(f"Write failed: {e}")

Catching the specific exception (FileNotFoundError, ValueError) rather than a bare except means you only handle the failures you expect and let genuine bugs surface. FileNotFoundError and PermissionError are both subclasses of OSError, so catching OSError covers the broad category of file-system problems. The !r conversion in the f-string shows each value with quotes, making it obvious whether you’re looking at the string '42' or something else.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull python:3.13-alpine

# Run the formatted output example
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python formatted_output.py

# Run the input example, piping in answers (-i keeps stdin open)
printf 'Ada\n36\n' | docker run --rm -i -v $(pwd):/app -w /app python:3.13-alpine python read_input.py

# Run the file read/write example
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python file_io.py

# Run the error-handling example
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python io_errors.py

The volume mount (-v $(pwd):/app) maps your current directory into the container, so files written by file_io.py—like notes.txt and output.txt—appear in your working directory after the run.

Expected Output

Running formatted_output.py:

Name: Ada, Age: 36
Pi rounded: 3.14
Zero-padded: 00036
Name: Ada, Age: 36
a-b-c
no newline here >>> same line

Running read_input.py with the piped input Ada and 36:

Enter your name: Hello, Ada!
Enter your age: Next year you will be 37

Running file_io.py:

Full contents:
First line
Second line
Third line

1: First line
2: Second line
3: Third line

Running io_errors.py:

Error: the file 'missing.txt' was not found
Parsed '42' -> 42
Could not parse 'hello' as an integer
Parsed '17' -> 17
Write complete

Key Concepts

  • print() is configurable — Use f-strings for interpolation, format specs like :.2f and :05d for numbers, and the sep/end parameters to control spacing and line endings.
  • input() always returns a string — Convert with int(), float(), etc. when you need a number; Python’s strong typing won’t do it for you.
  • Use with open(...) for files — The context manager closes the file automatically, even if an exception is raised, so you never leak file handles.
  • Mode strings pick the operation"r" reads, "w" overwrites, "a" appends; add "b" for binary data.
  • Iterate files line by line for large datafor line in f streams one line at a time instead of loading the whole file into memory with .read().
  • I/O errors are exceptions — Catch specific types like FileNotFoundError and ValueError; OSError is the shared base class for file-system failures.
  • Prefer specific except clauses — Handle only the errors you expect so real bugs aren’t silently swallowed.

Running Today

All examples can be run using Docker:

docker pull python:3.13-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining