~/troubleshooting/python-syntaxerror-invalid-syntax-the-7-most-common-causes
§ POST · MAY 16, 2026 v1.0

Python SyntaxError: invalid syntax – the 7 most common causes

The 7 patterns that cause 90% of SyntaxError: invalid syntax messages in Python, with the exact line you should look at first and the fix for each.
Ryan CallowayStaff contributor
  8 min read

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

  1. 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.
  2. 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.
  3. Check your Python version with python --version. New syntax (walrus, match, PEP 701 f-strings, type statement) on an old interpreter prints invalid syntax at the new operator.
  4. Run python -m py_compile yourfile.py for 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:

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

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.

esc