my_list = my_list.sort() doesn’t sort your list. It sets it to None.
data = [3, 1, 2]
data = data.sort()print(data) # None ← the list is goneThis is deliberate. In Python, a method that modifies an object in place returns None — precisely so you can’t chain it and lose the original by accident.
The trouble is that the design protects you from one mistake by creating another, and the second one is silent. The line above raises no error. The failure comes later, somewhere else, when something tries to use a list that is now None.
This is the full list of methods that behave this way, and why it matters more on a server than on your laptop.
| The rule | Modifies in place → returns None |
| The exceptions | pop() — it returns what it removed |
| The classic mistake | x = x.sort() — assigns None |
| Why it’s dangerous | No error at the line that caused it |
| Where it hurts | Cron jobs and deploys, hours after the fact |
Table of Contents
The rule behind it
Python separates two kinds of operation, and the return value is the signal.
Methods that modify the object — sorting it, adding to it, removing from it — return None. The work happened to the object itself.
Functions that build something new — sorted(), reversed(), list() — return the new thing and leave the original untouched.
data = [3, 1, 2]data.sort() # returns None, but data is now [1, 2, 3]
print(data) # [1, 2, 3]original = [3, 1, 2]
new = sorted(original) # returns a new list
print(original) # [3, 1, 2] ← untouched
print(new) # [1, 2, 3]⚠️ The naming is the clue. sort is a verb in the imperative — do this to the object. sorted is an adjective — give me a sorted version. Same for reverse and reversed.
The full list

Lists
| Method | Returns | Use instead, if you need a value |
|---|---|---|
append(x) | None | old + [x] |
extend(it) | None | old + list(it) |
insert(i, x) | None | Slicing |
remove(x) | None | A list comprehension |
sort() | None | sorted(x) |
reverse() | None | list(reversed(x)) or x[::-1] |
clear() | None | Assign a new empty list |
pop() | the item | — it already returns |
⚠️ pop() is the exception in every type. It modifies the object and returns what it removed — which is the whole point of it.
Dictionaries and sets
# Dictionaries
config = {"debug": False}
print(config.update({"port": 8080})) # None
print(config.clear()) # None
print({"a": 1}.pop("a")) # 1 ← returns# Sets
tags = {"python"}
print(tags.add("web")) # None
print(tags.discard("web")) # None
print(tags.update({"api"})) # None
print({1, 2}.pop()) # 1 ← returnsThe modern alternatives don’t mutate:
base = {"debug": False}merged = {**base, "port": 8080} # any version
merged = base | {"port": 8080} # Python 3.9+print(base) # {'debug': False} ← untouched
print(merged) # {'debug': False, 'port': 8080}And two more
import randomitems = [1, 2, 3]
print(random.shuffle(items)) # None ← shuffles in placeprint(print("anything")) # None ← print returns None toodef do_work():
passprint(do_work()) # None ← no return means None⚠️ random.shuffle() catches people because the equivalent that returns exists — random.sample(items, len(items)) gives a shuffled copy.
Why the bug is silent

This is the part that matters, and it’s why this isn’t just a style issue.
records = load_from_database()
records = records.sort() # ← the bug is here. No error.for r in records: # ← the error appears here
process(r)# TypeError: 'NoneType' object is not iterableThe assignment succeeds. None is a perfectly valid value, and Python has no reason to object.
The failure comes at the next line that tries to use the variable — which might be two lines later, or in a different function, or in a module imported somewhere else entirely.
And the error message points at the wrong place. 'NoneType' object is not iterable tells you where the code broke, not where it went wrong.
The variations you’ll see
TypeError: 'NoneType' object is not iterable
TypeError: 'NoneType' object is not subscriptable
AttributeError: 'NoneType' object has no attribute 'append'
TypeError: object of type 'NoneType' has no len()All four mean the same thing: something upstream returned None and you stored it. Our guide on None in Python covers the object itself.
Where this actually costs you: the server
On your laptop, you run the script, see the traceback, and fix it in thirty seconds.
On a server, three things change.
The job runs when nobody is watching
A scheduled task at 3am fails, writes a traceback to a log, and exits. Nothing alerts you. The report doesn’t arrive, the backup isn’t written, the sync doesn’t happen — and you find out the next morning, or the next week.
# In a cron job, this line ends the run
config = config.update(load_overrides())# Everything after it never executesThe output may never reach the log
If the script prints progress without a newline, the buffer holds it — and a crash can discard what was never flushed. ⚠️ That’s why a failed job sometimes leaves an empty log file, which is the worst possible amount of information.
The fix is one flag on the interpreter:
python3 -u script.py >> /var/log/myjob.log 2>&1-u disables buffering, and 2>&1 makes sure the traceback goes to the same file as the normal output.
And the damage may already be written
The worst case isn’t a crash — it’s a partial write.
for row in rows:
save_to_database(row) # writes 400 rowssorted_rows = rows.sort() # None
write_report(sorted_rows) # crashesFour hundred rows are already committed. The job died halfway, and rerunning it duplicates them.
⚠️ This is the argument for a staging environment, and for snapshots you can roll back to — because “just run it again” isn’t always safe.
How to avoid it
Read the return value as a signal
If a method name is an imperative verb, assume it returns None. sort, append, remove, update, clear, reverse, insert, extend, add, discard, shuffle.
If it’s a noun or an adjective, it probably returns something. sorted, reversed, copy, keys, items.
Never chain a mutating method
items.sort().reverse()
# AttributeError: 'NoneType' object has no attribute 'reverse'items.sort() # correct
items.reverse()Let a linter catch it
Both major linters have a rule for exactly this, and it runs in a second:
pip install ruff
ruff check .pip install pylint
pylint myscript.py⚠️ Running this in your deploy pipeline is the real fix. The bug is easy to make and easy to detect — the only hard part is noticing it in production.
And make failures visible
A cron job that fails silently is worse than one that fails loudly.
MAILTO=you@example.com
0 3 * * * /usr/bin/python3 -u /opt/job.py >> /var/log/job.log 2>&1MAILTO sends you anything the job writes to output. ⚠️ It requires a working mail setup on the server — which is exactly the thing nobody checks until they need it.
A job that dies halfway leaves data half-written, and rerunning it duplicates the rest. Copahost VPS plans include snapshots you take before every deploy and restore in minutes — plus root access, real cron, and logs you can tail. From €3.99/month, with your choice of Python version.
See VPS plansFrequently asked questions
Which Python methods return None?
Every method that modifies an object in place. On lists: append, extend, insert, remove, sort, reverse, clear. On dictionaries: update, clear. On sets: add, discard, update, clear. Plus random.shuffle() and print(). ⚠️ pop() is the exception in every type — it returns what it removed.
Why does my_list = my_list.sort() break my list?
Because sort() sorts in place and returns None, so the assignment stores None and the list is lost. Either call my_list.sort() on its own line, or use sorted(my_list), which returns a new sorted list and leaves the original alone.
Why does Python do this?
To make the distinction visible. A method that changes the object returns nothing, precisely so you can’t chain it and lose track of what was modified. The naming reinforces it: sort is an imperative verb, sorted is an adjective.
What’s the difference between sort() and sorted()?
sort() is a list method that sorts in place and returns None. sorted() is a built-in function that returns a new sorted list, works on any iterable, and leaves the original untouched. The same pattern applies to reverse() and reversed().
Why is this bug so hard to find?
Because the error appears somewhere else. The assignment itself raises nothing — None is a valid value. The failure comes at the next line that tries to use the variable, which may be in another function or another file, and the traceback points there, not at the cause.
What does 'NoneType' object is not iterable mean?
That you’re looping over something that is None. It almost always means a variable was assigned the result of a mutating method — data = data.sort() or similar — or that a function returned None when you expected a collection.
How do I add to a dictionary without losing the original?
Use unpacking: merged = {**base, "key": value}. On Python 3.9 or later, the | operator does the same: merged = base | {"key": value}. Both return a new dictionary and leave base untouched, unlike update().
How do I shuffle a list without modifying it?
random.shuffle() works in place and returns None. For a shuffled copy, use random.sample(items, len(items)), which returns a new list with the same items in random order.
Can a linter catch this?
Yes, and it takes a second. Both ruff and pylint have a rule for assigning the result of a method that returns None. Running one in your deploy pipeline catches the mistake before it ever reaches a server.
Why does this matter more on a server?
Because nobody is watching. A scheduled job fails at 3am, writes a traceback to a log, and exits — no alert, no notification. The report doesn’t arrive and you find out the next morning. ⚠️ And if the job wrote data before crashing, rerunning it can duplicate what it already saved.
Why is my cron log empty when the job failed?
Because Python buffers output when it isn’t writing to a terminal, and a crash can discard what was never flushed. Run the script with python3 -u to disable buffering, and add 2>&1 to send the traceback to the same file as the normal output.
Does print() return anything?
It returns None. Assigning it — result = print("hello") — stores None, not the text. If you want the string, build it with an f-string or join() and print it separately.
Conclusion
Python’s rule is consistent: a method that modifies an object returns None. It’s a deliberate design choice, and it protects you from a real mistake — chaining operations and losing track of what changed.
Three things follow from it. x = x.sort() destroys the list, and the same applies to append, update, remove and a dozen others. pop() is the exception in every type, because returning the removed item is its purpose. And the name is the clue — imperative verbs mutate and return nothing; nouns and adjectives return something new.
And the reason this is more than a style note: the bug is silent at the line that causes it. On your laptop that costs thirty seconds. In a job that runs at 3am, it costs a report that never arrived, a log that says nothing, and sometimes data written halfway.
One linter in the deploy pipeline catches all of it — which is a cheap fix for a problem whose only hard part is noticing.
