By Ryan Calloway. Updated May 2026.
The recurring r/learnpython “help me debug” threads on SyntaxError turn out to be one of seven things, almost every time. The fix is determined by the exact token Python underlines with the ^ caret. On Python 3.13 (October 2024), the messages are noticeably better than older versions thanks to PEP 657‘s precise error locations and 3.13’s color-highlighted tracebacks. This is the seven-cause shortlist, in the order they show up in real codebases, with the 30-second diagnosis routine that beats reading the file line-by-line.
Quick answer
Read the line above the one Python reported. The interpreter points at the token where it noticed the problem, not where the problem started. Eight times out of ten the missing piece is one of: a colon at the end of an if/def/for, an unclosed parenthesis on the line above, or a Python version mismatch (newer syntax running on an older interpreter). On Python 3.11+ the error message tells you which one.
How to diagnose in 30 seconds
- Read the caret. It points at the token Python did not expect. If the message says “expected X”, add X above and you are done.
- If the caret is on a line that looks fine, scroll up one line. Most of the time the cause is on the line before — usually an unclosed
(,[,{, or quote. - Check your Python version with
python --version. New syntax (walrus, match, PEP 701 f-strings,typestatement) on an old interpreter printsinvalid syntaxat the new operator. - Run
python -m py_compile yourfile.pyfor clean error reporting on long files where the traceback would otherwise scroll off-screen.
The Python 3.13 release notes describe the colored traceback output (controlled via PYTHON_COLORS / NO_COLOR env vars) which makes the caret much easier to find on a busy terminal.
Cause #1: missing colon
Every if, elif, else, for, while, def, class, try, except, finally, and with needs a trailing colon.
# Repro
if user == "admin"
print("hi")
# Python 3.11+ output
File "app.py", line 1
if user == "admin"
^
SyntaxError: expected ':'
# Fix
if user == "admin":
print("hi")
On Python 3.10 or older the message is invalid syntax with the same caret position. Either way the fix is the colon. The expected ':' wording was added in 3.10/3.11 by PEP 657 and is one of the strongest reasons to upgrade off the 3.9 line if you are still on it.
Cause #2: unclosed bracket, paren, or quote
The error line points at the line after the unclosed delimiter, not the line where it opened.
# Repro: the ( on line 1 is never closed
data = dict(
name="Ryan",
email="r@example.com"
print(data)
# Python 3.13 output
File "app.py", line 1
data = dict(
^
SyntaxError: '(' was never closed
Python 3.13 highlights the opening delimiter in the traceback (in color when supported). Older versions just say invalid syntax at the next non-trivial token. When the caret points at code that looks fine, scroll up — the bracket is open somewhere above. Use your editor’s bracket-match (% in Vim, click-then-Ctrl-Shift-\\ in VS Code) to find it.
# Fix
data = dict(
name="Ryan",
email="r@example.com",
)
print(data)
Cause #3: f-string quote collision (and PEP 701 in 3.12+)
Inside an f-string on Python 3.11 or older, you cannot reuse the same quote style as the f-string boundary.
# Repro on Python 3.11
msg = f"user {user["name"]}"
File "app.py", line 1
msg = f"user {user["name"]}"
^^^^
SyntaxError: f-string: unmatched '['
# Fix on any version
msg = f"user {user['name']}"
# On Python 3.12+ this also works (PEP 701)
msg = f"user {user["name"]}"
PEP 701 in Python 3.12 reimplemented the f-string parser; from 3.12 onward you can nest matching quotes, embed multi-line expressions, and even escape characters inside the expression part. Half the tutorials online still assume 3.11 syntax. If you need cross-version compatibility, keep the inner quotes different.
Cause #4: Python 2 syntax in a Python 3 interpreter
Copy-pasted from a 2014 blog post, or running an old script.
# Repro
print "hello"
File "app.py", line 1
print "hello"
^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
Python 3 calls out the exact issue with a Did you mean hint. Add the parentheses; check the timestamp of the source you copied from. The other Python 2 leftovers that show up the same way: except Exception, e: (now except Exception as e:), print >> sys.stderr, "msg", and xrange.
# Fix
print("hello")
Cause #5: version-gated syntax on an old interpreter
Some syntax is valid only on newer Python versions. Running it on an older interpreter prints a confusing invalid syntax pointing at the new operator.
| Syntax | Minimum Python |
|---|---|
f-strings (f"...") |
3.6 |
Walrus (:=) |
3.8 |
Positional-only params (/) |
3.8 |
Structural pattern matching (match/case) |
3.10 |
| Parenthesized context managers | 3.10 |
| Nested matching quotes inside f-strings | 3.12 |
type statement for aliases |
3.12 |
| Free-threaded mode (no GIL, experimental) | 3.13 |
# Repro on Python 3.9
if (n := len(data)) > 10:
print(n)
File "app.py", line 1
if (n := len(data)) > 10:
^
SyntaxError: invalid syntax
If you see invalid syntax pointing at :=, match, or a type X = ... alias, check python --version first. uv swaps interpreters in under five seconds; pyenv does the same on systems where uv is not installed yet.
Cause #6: leftover paste / merge tokens
You pasted code and left a stray >>> REPL prompt, a triple-backtick fence, or a Git merge-conflict marker.
# REPL prompt left in the file
>>> def greet(name):
... return f"hi {name}"
# Unresolved merge conflict
<<<<<<< HEAD
return value
=======
return normalize(value)
>>>>>>> feature-branch
Both produce SyntaxError: invalid syntax at the first non-Python line. Strip the prompts; resolve the conflict; the parser stops complaining. The Git merge conflict guide covers the second case end-to-end.
Cause #7: missing comma in a list, dict, or function call
The most under-recognized cause. Inside a collection or call, a missing comma between items reads to the parser as one expression continuing into the next, which usually parses to garbage and the caret lands mid-line.
# Repro: no comma after "Ryan"
user = dict(
name="Ryan"
email="r@example.com",
)
File "app.py", line 3
email="r@example.com",
^^^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?
Python 3.10+ added the Perhaps you forgot a comma? hint specifically for this case. Older versions just point at the second item with no explanation. Same pattern shows up in lists and tuples:
# Wrong: this is a list with one string "ababcd", not three items
items = ["a" "b" "c" "d"] # adjacent string literals concatenate
# Right
items = ["a", "b", "c", "d"]
The adjacent-string-concatenation feature is technically valid Python — "a" "b" is the same as "ab". It is a long-standing footgun in lists where you missed a comma; the result type-checks at runtime but is not what you intended. Ruff’s ISC001 rule flags this.
Decision tree: which cause is yours?
| Caret points at… | Most likely cause |
|---|---|
End of an if/def/for/etc. line, message says expected ':' |
#1 missing colon |
An open (, [, {, or the line after one that looks fine |
#2 unclosed delimiter |
| Inside an f-string, message mentions a quote or bracket | #3 f-string quote collision |
The word print with no parens |
#4 Python 2 syntax |
:=, match, type, or other recent operator |
#5 version-gated syntax |
>>>, <<<<<<<, or backtick |
#6 paste / merge marker |
| Mid-expression on the second item of a list/dict/call | #7 missing comma |
Catch them before runtime
Two tools that eliminate most of this class of error from your daily loop:
- Ruff on save. Ruff catches almost every
SyntaxErrorin milliseconds, before you press run. Sub-second feedback. Cross-platform. The repo crossed 30k stars in 2024 partly on this single feature. - An editor with a Python language server. Pylance, Pyright, or BasedPyright will underline these as you type. Five minutes of setup; years of quiet.
For projects where you can choose the runtime, Python 3.13 plus Ruff plus a language server is the lowest-friction setup in 2026 — the colored tracebacks alone make iterating on syntax errors faster.
FAQ
Why does Python 3.10 give me the vague “invalid syntax” message?
3.10 still uses the older parser for some message paths. 3.11 made the messages specific (expected ':', '(' was never closed, Perhaps you forgot a comma?). 3.13 adds colored highlighting on top. Upgrade if you can; it cuts SyntaxError debugging time in half on real workflows.
How do I fix “expected an indented block”?
The line after a colon (inside if, def, etc.) must be indented with either 4 spaces or a tab, consistently across the file. An empty body needs pass as a placeholder. Do not mix tabs and spaces — Python’s TabError appears as a SyntaxError variant on inconsistent indentation.
Can a missing comma cause this error?
Yes, and it is the seventh cause in the list above. Python 3.10+ even surfaces a Perhaps you forgot a comma? hint when it can guess. Adjacent string literals make this trickier in lists because the result is technically valid ("a" "b" concatenates to "ab") but produces the wrong number of items.
Why does my f-string work in one file but not another?
Different Python versions. Check python --version in the shell that ran each file; a venv with 3.10 and another with 3.12 will treat nested quotes inside f-strings differently because of PEP 701 in 3.12.
Should I use an IDE or Ruff to catch these before running?
Both. Editor language servers flag these as you type; Ruff catches the rest on save and adds a pre-commit hook for free. Five minutes of setup, years of quiet. The combination eliminates most of the SyntaxError class from a daily workflow.
Is the free-threaded build of Python 3.13 a SyntaxError risk?
No. Free-threaded mode (3.13’s experimental no-GIL build) does not change the language grammar. Code that parses on stock 3.13 parses the same on the free-threaded build; runtime behavior around threading is what differs.
Sources and further reading
- What’s new in Python 3.13 — colored tracebacks, free-threaded mode, JIT
- What’s new in Python 3.12 — PEP 701 f-strings,
typestatement - PEP 657 — Include Fine Grained Error Locations in Tracebacks
- PEP 701 — Syntactic formalization of f-strings
- Python docs —
SyntaxError - Ruff — fast Python linter
- uv — fast Python package and version manager
Next steps
If the syntax is clean but the program still refuses to run, the next error you usually hit is ModuleNotFoundError or TypeError: NoneType is not subscriptable. The Python virtual environment guide shows how a per-project interpreter setup prevents version-gated syntax surprises in the first place.