Ever tried reading a Python script and felt like you were looking at a recipe with no clear steps? You're not alone. Most beginners hear the word "procedure" and assume it's some heavy computer-science concept locked behind a textbook.
Turns out, a procedure in Python is one of the most practical things you'll touch — even if you've already been using one without knowing the name.
What Is a Procedure in Python
Here's the thing — Python doesn't officially use the word "procedure" the way older languages like Pascal did. Also, in Python, what people call a procedure is usually just a function that doesn't return a value. That's it. No secret handshake That alone is useful..
Look, in some languages a "function" gives you something back, and a "procedure" just does something and walks away. Plus, python blurs that line because every function returns something — even if it's just None when you don't write a return statement. So when someone says "procedure" in a Python context, they mean a block of reusable code you call to perform an action: print stuff, save a file, update a record, send a request Still holds up..
And that's the short version. But it's worth knowing where the term came from, because it explains why your coworker might still use it in 2024.
Where the Word Comes From
Older programming languages made a hard split. A function computed a value. Which means a procedure executed steps. You'd call a procedure to make something happen in the world — move a robot arm, write to disk — and call a function when you needed a number or a string back.
Python folded both into one idea: the def block. But the habit of calling "do-something" code a procedure stuck around. Honestly, this is the part most guides get wrong — they pretend the word doesn't exist in Python. It does, as slang.
Procedure vs Function in Python Speech
In daily Python talk:
- A function returns a result you'll use (
def add(a, b): return a + b). - A procedure-style function does work and returns nothing useful (
def log_error(msg): print(msg)).
Real talk, the interpreter doesn't care. But you'll care when you're reading code at 2 a.Practically speaking, m. and trying to figure out if a call gives you data or just side effects And that's really what it comes down to..
Why It Matters / Why People Care
Why does this matter? Because most people skip the difference between "doing" and "returning" — and then their code turns into spaghetti.
I know it sounds simple — but it's easy to miss. Think about it: if you think every function should hand you something back, you'll start returning weird placeholders from procedures just to be safe. Or worse, you'll write procedures that secretly mutate global state and return None, and nobody will know what ran or why The details matter here. Simple as that..
Here's a concrete mess I've seen: a script that "saves the report" by calling save_report() which prints "done" and writes a file. No return. Which means the caller then tries if save_report(): — which is always falsy. That said, bug. Confusion. A Friday wasted Less friction, more output..
Understanding the procedure mindset helps you:
- Name things clearly (
send_emailvsbuild_email) - Spot side effects fast
- Test code without pretending a void action returned a value
- Talk to older devs or read legacy code that uses the term seriously
In practice, knowing this split makes your code honest. In practice, a procedure says "I did a thing. " A function says "here's your thing.
How It Works (or How to Do It)
The meaty middle. Let's build the idea from scratch so it actually sticks Worth keeping that in mind..
Defining a Procedure-Style Block
You write it like any function. Use def, give it a name, open a body.
def greet_user(name):
print(f"Hey {name}, welcome back.")
Call it: greet_user("Sam"). Because of that, it returns None. It prints. That's a procedure in Python clothing.
No return line needed. Think about it: if you add return with nothing after it, same result. If you never mention return, Python quietly hands back None anyway Turns out it matters..
Calling and Ignoring the Return
When you call a procedure, you usually don't capture the result.
greet_user("Sam") # we just want the print
But this is also valid and silly:
result = greet_user("Sam") # result is None
Beginners do this thinking they "saved" the output. They didn't. The output went to the screen, not to the variable.
Procedures With Side Effects
Most real procedures touch the outside world. Files, databases, networks, LEDs on a Raspberry Pi And that's really what it comes down to..
def write_note(path, text):
with open(path, "a") as f:
f.write(text + "\n")
That's a procedure. Plus, it changes a file. Returns nothing. You call it because you want the file changed, not because you want a trophy The details matter here..
When Python Forces a Return Anyway
Here's a subtle bit. Even pure procedures return None. So if you chain them, you get None in the chain.
def step_one():
print("one")
def step_two():
print("two")
step_one() and step_two() # step_two never runs; None is falsy
Look, that's a footgun. If you meant both to run, don't rely on return values from procedures. Just call them on separate lines.
Turning a Procedure Into a Function (and Back)
Sometimes a procedure should've been a function. Example: you log an error and want the message back for tests.
def log_error(msg):
print(f"ERROR: {msg}")
return msg # now it's a function too
Now it does the action and returns. Is it still a procedure? In strict terms, no. In Python team slang, who cares — it's useful That's the whole idea..
The point is: you choose. And action-only? Procedure style. In real terms, need the data? Return it.
Common Mistakes / What Most People Get Wrong
This section builds trust because the errors are specific, not vague.
Mistake 1: Thinking "procedure" is a reserved word. It isn't. You can't write procedure greet():. Python will yell. Use def.
Mistake 2: Returning None explicitly to "be safe". I've reviewed code like return None at the end of every void block. It adds noise. If you have nothing to return, don't. The interpreter already does it And it works..
Mistake 3: Using procedure output in logic. We covered this. if send_email(): is false even when the email sent. Then your app thinks it failed. Track success with a real return or an exception, not a void call And that's really what it comes down to..
Mistake 4: Hidden global mutation. A procedure that edits a global list without saying so is a trap.
users = []
def add_user(name):
users.append(name) # silent side effect
Callers don't see the dependency. Pass the list in, or return a new one Most people skip this — try not to..
Mistake 5: Confusing print with return. The classic. New devs think print gives the value to the caller. It doesn't. Print is for humans. Return is for code Easy to understand, harder to ignore..
Practical Tips / What Actually Works
Skip the generic advice. Here's what actually helps when you're writing or reading Python every day.
- Name procedures like actions.
render_page,sync_files,close_socket. If it returns data, name it like a question or getter:get_user,calculate_tax. - Put procedures and functions in different mental buckets. When reviewing a pull request, ask: does this need a result? If not, don't test the return — test the side effect (file exists, DB row present).
- Use type hints to signal intent.
def save_config(path: str) -> None:tells everyone "this is a procedure, don't expect a value." That-> Noneis free documentation. - Don't catch
Nonelike it's data. If a function returnsNoneon purpose, handle absence explicitly. Don't let it flow into math
Keep the Signature Tight
When a procedure is meant to only do work, keep its signature lean. But a single‑argument “config” object, a path, or a list of items is fine, but avoid passing the entire application context unless you really need it. The fewer inputs a procedure has, the easier it is to reason about its side effects.
def purge_cache(cache: Cache) -> None:
cache.clear()
If you findnr a procedure needs more than a handful of parameters, refactor it into a small helper or split it into multiple steps.
put to work Exceptions for Failure
A silent return None is a common way to signal “something went wrong.” It is almost always better to raise an exception, which forces callers to acknowledge the failure Easy to understand, harder to ignore..
def load_config(path: str) -> Dict[str, Any]:
if not os.path.exists(path):
raise FileNotFoundError(f"Config file {path} missing")
return json.load(open(path))
Catch the exception only where you can actually recover or provide a meaningful error message. This keeps the normal flow free of hidden failure paths And that's really what it comes down to..
Document Intent in the Docstring
A well‑written docstring is the first line of documentation for a procedure. Use the first sentence to describe the action, and if it returns something, note that explicitly That alone is useful..
def send_notification(user_id: int, message: str) -> None:
"""
Send an email notification to the user with the given ID.
Parameters
----------
user_id : int
The user’s unique identifier.
message : str
The body of the email.
Returns
-------
None
This function performs a side effect only; it does not return a value.
"""
When a function returns data, start the docstring with a short description of the value, not the action Not complicated — just consistent..
Test for Side Effects, Not Return Values
Procedures are usually verified by inspecting the world after they run That's the part that actually makes a difference..
def test_save_config(tmp_path):
config_path = tmp_path / "config.yaml"
save_config(config_path, {"debug": True})
assert config_path.read_text() == "debug: true\n"
Notice the absence of a return value assertion. For functions that return data, write a separate test that checks the value directly No workaround needed..
Avoid “Procedural” Global State
If you must use global variables, wrap them in a container and pass that container to the procedure. This makes the dependency explicit and keeps the global namespace clean That's the part that actually makes a difference..
class AppState:
def __init__(self):
self.users: List[User] = []
def add_user(state: AppState, user: User) -> None:
state.users.append(user)
Now callers know exactly what will change, and you can mock or replace the state in tests.
Keep the “Procedure” and “Function” Distinction in Mind, But Don’t Over‑Enforce It
In practice, the line between a procedure and a function blurs. A helper that logs something and returns a value is perfectly fine. The key is clarity: if a caller can’t use the return value, document that it’s a side‑effect‑only routine The details matter here..
Wrapping It All Up
Procedures and functions are just two sides of the same coin: code that does something. The decision to return a value or not should be guided by intent, not by language constraints. By naming your routines clearly, using type hints to signal expectations, handling failures with exceptions, and testing for the right kind of outcome, you keep your codebase understandable and maintainable Small thing, real impact..
Remember:
- Name for action → procedure
- Name for data → function
- Return
Noneonly when you truly have nothing - Document intent with concise docstrings
- Test side effects for procedures, data for functions
- Keep dependencies explicit
With these habits, your code will stay clean, your team will understand your intent, and you’ll spend less time chasing after hidden bugs. Happy coding!
A Practical Example: Putting the Guidelines Together
Consider a small service that processes uploaded files. One routine merely moves the file to a storage bucket and records the event, while another calculates a checksum for later verification.
def archive_upload(file_path: Path, bucket: str) -> None:
"""Move the uploaded file to the given storage bucket and log the action."""
shutil.move(str(file_path), f"{bucket}/{file_path.name}")
logger.info("Archived %s to %s", file_path.name, bucket)
def compute_checksum(file_path: Path) -> str:
"""Return the SHA-256 hex digest of the file's contents."""
return hashlib.sha256(file_path.read_bytes()).
The first is a procedure: its value is in what it does to the filesystem and the logs. The second is a function: its entire point is the returned string. A teammate reading the callsite immediately knows not to expect anything back from `archive_upload`, and to capture the result of `compute_checksum`.
## Conclusion
Treating procedures and functions as distinct, intentional shapes of code is less about rigid rules and more about communication. Also, ” Answer that, name and document accordingly, and let type hints and tests reinforce the contract. When you write a routine, ask: “What is the one thing the caller should care about—the changed world or the delivered value?Over time, these small acts of clarity compound into a codebase where side effects are visible, returns are meaningful, and everyone—including future you—can reason about the code without guesswork.