When I first started writing Python, the very first two functions I learned were print() and input(). They seem trivial at a glance — one shows text on the screen, the other reads text from the keyboard — but the truth is that a huge percentage of real-world Python programs are built almost entirely around input and output operations. Whether I’m building a command-line tool, logging data from a sensor, or writing a script that reads a configuration file and writes a report, I’m doing I/O. In this guide, I want to walk through everything I know about basic input and output in Python, from the simplest print("Hello") statement all the way to buffered file handling and encoding issues that trip up even experienced developers.
Why I/O Matters So Much in Programming
Every program exists to do something with data — and that data has to come from somewhere and go somewhere. I/O (Input/Output) is the mechanism that connects my program to the outside world: the user’s keyboard, the terminal screen, files on disk, network sockets, and more. Without I/O, a program is just a closed box that computes something nobody will ever see.
Python was designed with readability in mind, and that philosophy shows clearly in how it handles I/O. Compared to languages like C or Java, Python’s print() and input() are refreshingly simple, but under the hood there’s quite a bit going on that I think is worth understanding.
The print() Function: More Than Meets the Eye
At its simplest, I can write:
print("Hello, World!")
Output:
Hello, World!
But print() in Python 3 is a full function with several useful parameters that I use constantly:
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
Let me break these down with examples.
Multiple Arguments and the sep Parameter
name = "Ali"
age = 28
print("Name:", name, "Age:", age)
Output:
Name: Ali Age: 28
By default, Python separates each argument with a single space. I can change that using sep:
print("2026", "07", "30", sep="-")
Output:
2026-07-30
This is genuinely useful when I’m formatting dates, CSV-like output, or joining path segments on the fly.
The end Parameter
By default, print() appends a newline character (\n) after the output. I can override this:
print("Loading", end="")
print("...", end="")
print("Done")
Output:
Loading...Done
I use this pattern a lot when building simple progress indicators in scripts that don’t need a full progress bar library.
Formatted Output with f-strings
Since Python 3.6, f-strings have become my default way of formatting output because they’re readable and fast:
name = "Sara"
score = 92.5678
print(f"{name} scored {score:.2f}%")
Output:
Sara scored 92.57%
I can also use f-strings for alignment, which is handy when I’m printing tabular data:
items = [("Apple", 3), ("Banana", 12), ("Fig", 1)]
for name, qty in items:
print(f"{name:<10}{qty:>5}")
Output:
Apple 3
Banana 12
Fig 1
Internal Working of print()
Internally, print() calls str() (or repr() in edge cases inside containers) on each object passed to it, joins them with sep, appends end, and writes the resulting string to the file object — which defaults to sys.stdout. That’s why I can redirect print output to any file-like object simply by passing file=my_file.
with open("log.txt", "w") as f:
print("This goes into a file, not the terminal", file=f)
The input() Function: Reading From the User
input() is Python’s built-in way of pausing execution and waiting for the user to type something and press Enter.
name = input("Enter your name: ")
print(f"Hello, {name}!")
If I run this and type “Zain”, the output is:
Enter your name: Zain
Hello, Zain!
input() Always Returns a String
This is one of the most common mistakes I see beginners make. input() never returns an integer or a float — it always returns a string, even if the user types digits.
age = input("Enter your age: ")
print(type(age)) # <class 'str'>
To use it as a number, I have to convert it explicitly:
age = int(input("Enter your age: "))
next_year_age = age + 1
print(f"Next year you'll be {next_year_age}")
If the user types something that isn’t a valid integer, this raises a ValueError, so in real programs I usually wrap it in a try/except block:
while True:
try:
age = int(input("Enter your age: "))
break
except ValueError:
print("That's not a valid number. Try again.")
File I/O: Reading and Writing Files
Once I move past the console, the next natural step is working with files. Python’s open() function is the entry point for all file operations.
Opening a File
file = open("notes.txt", "w")
file.write("This is my first line.\n")
file.close()
I always try to avoid this pattern because it’s easy to forget file.close(), which can lead to unflushed buffers or file locks that linger. Instead, I use the with statement, which automatically closes the file even if an exception occurs:
with open("notes.txt", "w") as file:
file.write("This is my first line.\n")
file.write("This is my second line.\n")
File Modes I Use Regularly
| Mode | Meaning |
|---|---|
'r' | Read (default) — file must exist |
'w' | Write — creates file or truncates existing one |
'a' | Append — writes are added to the end |
'x' | Exclusive creation — fails if file exists |
'b' | Binary mode, combined with others like 'rb' |
'+' | Read and write, combined like 'r+' |
Reading Files
with open("notes.txt", "r") as file:
content = file.read()
print(content)
If the file is large, I don’t want to load the whole thing into memory at once. Instead, I iterate line by line, which is memory-efficient because Python reads the file lazily using an internal buffer:
with open("notes.txt", "r") as file:
for line in file:
print(line.strip())
I can also use readlines() to get a list of all lines, or readline() to read just one line at a time:
with open("notes.txt", "r") as file:
first_line = file.readline()
print(first_line)
Appending to Files
with open("notes.txt", "a") as file:
file.write("This line was appended later.\n")
Internal Working: Buffering and Encoding
Something I didn’t appreciate early on is that Python’s file objects are buffered by default. When I call file.write(), the data doesn’t necessarily hit the disk immediately — it sits in an internal buffer until the buffer is full, the file is closed, or I explicitly call file.flush(). This matters if I’m writing logs in real time and need the data visible immediately to another process:
with open("live_log.txt", "w") as f:
f.write("Starting process...\n")
f.flush()
Encoding is another subtlety. On most modern systems, Python defaults to UTF-8, but this isn’t guaranteed across every platform. I’ve learned to be explicit whenever I care about portability:
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
Performance Considerations
For small scripts, the performance of print() and file I/O rarely matters. But once I’m processing large amounts of data, a few things make a real difference:
- Batching writes: Calling
file.write()in a tight loop thousands of times is slower than building a list of strings and calling"".join(lines)once, then writing that single string. - Using
sys.stdout.write()instead ofprint()in performance-critical loops, sinceprint()has extra overhead from argument parsing and default newline handling. - Reading in chunks for very large files rather than loading everything with
read(), especially for binary files like images or large CSVs.
CHUNK_SIZE = 1024 * 1024 # 1 MB
with open("bigfile.bin", "rb") as f:
while chunk := f.read(CHUNK_SIZE):
process(chunk)
Real-World and Automation Use Cases
I use these fundamentals constantly in day-to-day automation work:
- Writing a script that reads a list of URLs from a text file and processes each one.
- Logging the progress of a long-running data pipeline to a file so I can monitor it later.
- Building simple CLI tools that ask the user for input, validate it, and act on it.
- Generating reports by writing formatted text or CSV rows to disk.
- Reading configuration values from a
.envor.inifile at startup.
Common Mistakes I See (and Have Made Myself)
- Forgetting to close files. Always use
with open(...) as f:instead of manually callingopen()andclose(). - Assuming
input()returns a number. It always returns a string — convert explicitly. - Not handling
ValueErrorfromint(input()). Wrap conversions in try/except when user input is unpredictable. - Overwriting files accidentally. Opening in
'w'mode instead of'a'mode wipes existing content — I double-check this every time. - Ignoring encoding. Not specifying
encoding="utf-8"can causeUnicodeDecodeErroron some systems, especially Windows.
Best Practices I Follow
- Always use the
withstatement for file operations. - Be explicit about encoding when reading or writing text files.
- Validate and sanitize user input before using it in calculations, file paths, or system commands.
- Use f-strings for clean, readable output formatting.
- Separate concerns: keep I/O code separate from business logic so it’s easier to test.
FAQs
Q: Can print() write to a file directly? Yes, by passing file=my_file_object as an argument.
Q: Does input() block program execution? Yes, execution pauses until the user presses Enter.
Q: What happens if I open a file that doesn’t exist in 'r' mode? Python raises a FileNotFoundError.
Q: How do I read a file without loading it entirely into memory? Iterate over the file object directly (for line in file:) or read it in fixed-size chunks.
Q: What’s the difference between 'w' and 'a' mode? 'w' truncates the file to zero length before writing; 'a' preserves existing content and adds to the end.
Troubleshooting Tips
- If you get a
PermissionError, check whether the file is open in another program or whether you have write access to that directory. - If text looks garbled, check the file’s encoding — try opening it explicitly with
encoding="utf-8"orencoding="latin-1". - If your script hangs unexpectedly, check whether it’s waiting on an
input()call you forgot about.
Summary
Input and output might be the first thing I learned in Python, but I still rely on these exact fundamentals every single day — whether I’m printing debug output, reading a config file, or writing logs for a production job. Getting comfortable with print(), input(), and file handling through the with statement gives me a solid foundation for everything else I build in Python.
