Best Code Rule: Always Separate Input, Output, and Processing
By Volodymyr Obrizan on August 22, 2025 · Read this post in other languages: Ukrainian Russian
When youʼre just starting out, writing code usually means “make it work.” So you throw together a quick script that asks the user for input, does something clever, and prints the result. Boom. Done.
But hereʼs the thing: that quick-and-dirty style will hurt you the moment your program needs to grow, change, or be reused.
Thereʼs one simple rule that separates messy scripts from clean, professional code:
Always separate input, output, and processing.
This rule is a game-changer. It helps you write code thatʼs easier to: test, debug, reuse, and build on later (extend).
You donʼt need to be a senior developer to follow it. You just need to see the difference. Letʼs walk through a real example — processing some tabular data — and youʼll never write glue-all-the-things code again.
In this blog-post:
- Step 1: You Write a Naive but Working Script
- Step 2: Now You Need to Read from a CSV File
- Step 3: Realization Hits: You Just Copied and Tweaked the Same Logic
- Always separate input, output, and processing
- Step 4: You Refactor the Processing Out of the Input Mess
- Step 5: You Refactor the File Version Too
- Step 6: You Go All In — Separate Input, Output, and Processing
- Step 7: Look at What Youʼve Gained
- Final Takeaway
Step 1: You Write a Naive but Working Script
The algorithm is pretty simple: read the value from keyboard — accumulate — repeat — divide by count:
total = 0
count = 0
while True:
line = input("Enter salary or 'done': ")
if line == "done":
break
total += float(line)
count += 1
print(f"Average salary: {total / count}")
It is simple and running this code gives us exactly what we wanted — the average salary for a set of inputs:
Enter salary or 'done': 100
Enter salary or 'done': 150
Enter salary or 'done': 200
Enter salary or 'done': 400
Enter salary or 'done': 300
Enter salary or 'done': done
Average salary: 230.0
It seems our job is done and done well.
Step 2: Now You Need to Read from a CSV File
Next day you realize that entering salary data from keyboard is not convenient. Youʼd like to read some widely adopted data format — CSV — like this one:
name,salary
Alice,50000
Bob,60000
Charlie,55000
So you think: “Okay, Iʼll just copy-paste and adapt my code to read from a file instead of input().” Hereʼs what you end up doing:
import csv
total = 0
count = 0
with open("employees.csv") as file:
reader = csv.DictReader(file)
for row in reader:
total += float(row["salary"])
count += 1
print(f"Average salary: {total / count}")
It is simple and it works! But wait...
Step 3: Realization Hits: You Just Copied and Tweaked the Same Logic
Now your logic to calculate the average salary exists in two places: once for user input, once for CSV file. Same math. Same structure. Duplicated. Slightly different sources.
Consider the following changes:
- What if you want to change how the average is calculated?
- What if you want to get data from an API tomorrow?
- What if you want to save results to a database?
With the current approach where input, processing, and output glued together youʼll copy-paste-adopting again and again, drowning in similar but different versions of code.
And hereʼs the kicker: you canʼt reuse any of it. You canʼt feel this power of synergy, when you can combine different functions to produce some new valuable effect in almost no time!
This is the dead-end. Youʼve written code thatʼs locked to its input. You canʼt test it, you canʼt extend it, and if you want to use it somewhere else — you start from scratch.
Always separate input, output, and processing
When youʼre just starting out, itʼs tempting to write everything in one big block: read input, do the work, and print the result — all tangled together. It works, sure. But it becomes a problem the moment you want to change how or where the data comes from, or what you do with it.
Letʼs look at the original example again. This code does three things at once.
Input:
input("Enter salary or 'done': ")
Youʼre reading data from the user. This is input. But input isnʼt just input()
from the keyboard. In real projects, data might come from: a file, a JSON API response, a web form, a command-line argument, a database query, a sensor or hardware device, even drag-and-drop in a UI. All of these are just ways data enters your program. And none of them should change your processing logic.
If youʼve mixed input and logic together, youʼll have to rewrite everything when the source changes — and thatʼs the mess weʼre trying to avoid.
Processing:
total += float(line) and total / count
Youʼre doing the work: converting strings to numbers, summing, counting, averaging. But itʼs mixed into the loop where youʼre reading input.
Output:
print(f"Average salary: {total / count}")
Youʼre showing the result. This is output. It also depends on the earlier code being written just so. In this case, the program prints the result to the screen — but output can take many other forms: writing to a file (CSV, JSON, text), sending data to a webpage or mobile app, returning a value from a function, inserting rows into a database, rendering a graph or chart, sending a network response (e.g., API or socket), updating a UI element on the screen.
So hereʼs the golden rule:
- Input = how data enters the program
- Processing = what your program does with that data
- Output = how results are delivered (print, save, return, etc.)
Keep them separate. Always. Each should be its own clean part of your code — ideally a function — that does just one job. By keeping input, processing, and output separate, you gain flexibility.
Letʼs proceed.
Youʼve just realized youʼre duplicating logic. It works... but youʼre writing the same code twice, and any change means changing it twice. Time for the plot twist.
Step 4: You Refactor the Processing Out of the Input Mess
Instead of having salary math tangled up in user input or file reading, you pull it into its own function. We use type annotations here to highlight what the processing function accepts as an argument (a list of floats list[float]
) and what it returns (float
):
def calculate_average(salaries: list[float]) -> float:
return sum(salaries) / len(salaries)
And now you update your original input version to use it:
salaries: list[float] = []
while True:
line = input("Enter salary or 'done': ")
if line == "done":
break
salaries.append(float(line))
print(f"Average salary: {calculate_average(salaries)}")
Much cleaner already. Your input is still a little messy, but the processing logic is now a reusable building block. Progress.
Step 5: You Refactor the File Version Too
Now you reuse that same function with the CSV data:
import csv
salaries: list[float] = []
with open("employees.csv") as file:
reader = csv.DictReader(file)
for row in reader:
salaries.append(float(row["salary"]))
print(f"Average salary: {calculate_average(salaries)}")
Boom. Now both your input-based and file-based scripts use the same core logic.
Youʼre starting to separate concerns without even using the term “separation of concerns.”
Step 6: You Go All In — Separate Input, Output, and Processing
Hereʼs the cleanest version so far:
import csv
def get_salaries_from_input() -> list[float]:
salaries = []
while True:
line = input("Enter salary or 'done': ")
if line == "done":
break
salaries.append(float(line))
return salaries
def get_salaries_from_csv(filename) -> list[float]:
with open(filename) as file:
reader = csv.DictReader(file)
return [float(row["salary"]) for row in reader]
def calculate_average(salaries: list[float]) -> float:
return sum(salaries) / len(salaries)
def print_average(salaries: list[float]) -> None:
average = calculate_average(salaries)
print(f"Average salary: {average}")
Please note that get_salaries_from_input
and get_salaries_from_csv
both returns a list of floats (list[float]
) so basically they do the same thing (getting us input data), but in different way: keyboard input and reading from a file.
Now you can choose your input source:
# Input data manually:
salaries = get_salaries_from_input()
print_average(salaries)
# Reading from file:
salaries = get_salaries_from_csv("employees.csv")
print_average(salaries)
Step 7: Look at What Youʼve Gained
You can now:
- Swap input sources without touching processing
- Test processing in isolation
- Reuse processing for APIs, web forms, GUIs, you name it
- Avoid duplication of logic
- Write cleaner, more maintainable code
For example, look how it is easy now to test your code with pytest
:
import pytest
from your_module import calculate_average
def test_calculate_average_basic():
data = [50000, 60000, 55000]
result = calculate_average(data)
assert result == 55000
def test_calculate_average_single_entry():
assert calculate_average([42000]) == 42000
def test_calculate_average_zero_values():
assert calculate_average([0, 0, 0]) == 0
def test_calculate_average_raises_on_empty_list():
with pytest.raises(ZeroDivisionError):
calculate_average([])
Otherwise, trying to test code that mixes input and processing is like trying to debug a blender while itʼs running. When your logic is tangled up with input()
, file reading, or user clicks, you canʼt easily feed in test data — you have to simulate real input every time. That means writing awkward workarounds, mocking built-ins, or worse: manually entering values during each test run. Itʼs fragile, slow, and frustrating. And because you canʼt isolate the “thinking” part of your code, bugs slip through, edge cases get missed, and refactoring becomes risky. Testing should be surgical — but you canʼt do surgery with spaghetti.
Consider code complexity: letʼs say your program supports 4 input sources — maybe user input, CSV files, a JSON API, and a database — and 3 output formats — like printing to console, saving to a file, or sending over a web response. If you mix your logic with both input and output, youʼre now facing 4 × 3 = 12 different code paths to manage. Twelve places where bugs can hide. Twelve places where small changes multiply into big maintenance headaches. Now imagine adding just one more input source — now youʼre juggling 15 combinations. Two more outputs? Now itʼs 20. This kind of growth is exponential, not linear. But if you cleanly separate input, output, and processing, you only need to write 4 input adapters, 3 output handlers, and 1 solid function that does the real work. One brain. Zero duplication. Infinite relief.
This is not over-engineering. This is minimal, clean separation — and itʼs all you need to escape the mess of glue-code scripts.
Final Takeaway
If you remember just one thing from this post, make it this:
Donʼt mix input, processing, and output. Ever.
Even in small scripts, this separation saves time and keeps your code flexible. Especially as your projects grow.