If you’re still running Python 2, or you’ve stumbled across old code that uses it, this guide is for you. I’ll walk through what actually changed between Python 2 and Python 3, why those changes matter in practice, and why upgrading isn’t optional anymore — it’s overdue.
The Short Answer First
Python 2 officially reached end-of-life on January 1, 2020. That means no more security patches, no more bug fixes, and no more official support — from anyone. If you’re running Python 2 in production today, you’re running software with known, unfixed vulnerabilities.
Python 3 isn’t just a newer version. It was a deliberate redesign of the language to fix problems that couldn’t be fixed without breaking backward compatibility. Most of those fixes make your code cleaner, more predictable, and less error-prone.
A Quick History Lesson
Python 3 was first released in December 2008. At the time, Python 2 was everywhere — in tutorials, libraries, servers, and university courses. The migration was slow because Python 3 wasn’t backward compatible, which meant existing code didn’t just “work” on Python 3 without changes.
For years, the Python community ran both versions in parallel. Libraries maintained two codebases. Developers argued about which version to teach beginners. It was messy.
By 2020, that era was over. Python 2 was sunset, and the ecosystem moved on. Today, every major library — NumPy, Pandas, Matplotlib, Django, Flask, Requests — has dropped Python 2 support entirely. You simply cannot run modern Python code on Python 2.
The print Statement vs print() Function
This is the change most people encounter first, and it looks trivial, but it’s actually meaningful.
In Python 2, print was a statement:
# Python 2
print "Hello, World"
print "Value:", x
In Python 3, print is a function:
# Python 3
print("Hello, World")
print("Value:", x)
Why does this matter? Because making print a function means it follows consistent rules — you can pass it as an argument, you can override it, and you can use keyword arguments like sep and end:
# Python 3 only
print("one", "two", "three", sep=" | ") # Output: one | two | three
print("Loading", end="...") # Output: Loading...
You can see the output in the screenshot below.

None of that is possible with the Python 2 statement syntax. It seems like a small thing, but it’s a good example of the overall philosophy of Python 3: make the language consistent and composable.
Integer Division — A Silent Bug in Python 2
This is the change that causes the most hidden bugs when people port Python 2 code. In Python 2, dividing two integers always gives you an integer:
# Python 2
10 / 3 # Returns 3, not 3.333...
7 / 2 # Returns 3, not 3.5
Python just silently discards the remainder. No warning, no error. This is called “floor division” and it’s the source of countless subtle bugs in Python 2 codebases — especially in anything that does calculations.
In Python 3, division works the way math actually works:
# Python 3
10 / 3 # Returns 3.3333333333333335
7 / 2 # Returns 3.5
If you actually want floor division in Python 3, use the // operator explicitly:
10 // 3 # Returns 3
7 // 2 # Returns 3
This is a much better design because you’re explicit about what you want. No surprises.
Unicode and Strings — The Biggest Architectural Change
This is the biggest and most important difference between the two versions.
In Python 2, there are two string types:
str— a sequence of raw bytes (ASCII by default)unicode— actual Unicode text
The problem? Python 2 tries to be “helpful” by silently converting between them, and that conversion frequently fails in confusing ways — especially when dealing with non-English text, file I/O, or web applications. If you’ve ever seen a UnicodeDecodeError in Python 2, you know exactly what I’m talking about.
# Python 2 — this can blow up unexpectedly
name = u"Ñoño"
print "Hello, " + name # UnicodeDecodeError in certain contexts
In Python 3, there’s one string type:
str— always Unicode, always
Bytes are a completely separate type (bytes), and Python 3 never silently converts between them. If you try to mix them, you get a clear TypeError immediately:
# Python 3 — explicit and predictable
text = "Hello, Ñoño" # str — Unicode, no special prefix needed
raw = b"some binary data" # bytes — binary data
text + raw # TypeError: can only concatenate str (not "bytes") to str
This might seem stricter, but it’s enormously better in practice. You always know what type you’re working with. No silent data corruption. No mysterious encoding errors at 2am.
This change is especially important if you’re building web apps, working with APIs, processing files with international characters, or doing any kind of data pipeline work.
input() vs raw_input()
In Python 2, there are two functions for getting user input:
- raw_input() — reads input as a string (this is what you actually want)
- input() — evaluates the input as a Python expression (this is almost always a security risk)
# Python 2
age = raw_input("Enter your age: ") # Returns a string "25"
result = input("Enter an expression: ") # Evaluates "2 + 2" and returns 4
The Python 2 input() is genuinely dangerous — if a user types __import__(‘os’).system(‘rm -rf /’), it executes it. Many Python 2 tutorials used input() by mistake, introducing security holes in beginner code.
In Python 3, this was cleaned up completely:
- input() always returns a string — safe, predictable
- raw_input() doesn’t exist anymore
- If you want to evaluate an expression, you use eval() explicitly, making the intent obvious
# Python 3
age = input("Enter your age: ") # Always returns "25" as a string
You can see the output in the screenshot below.

range() vs xrange()
In Python 2, there are two ways to create a sequence of numbers:
range()— creates the full list in memory immediatelyxrange()— creates a lazy iterator (generates numbers one at a time)
For large ranges, range(1000000) in Python 2 creates a list of one million integers in memory all at once. If all you’re doing is looping through them, that’s wasteful.
In Python 3, xrange() is gone, and range() behaves like the old xrange() — it’s a lazy iterator by default:
# Python 3
for i in range(1000000):
print(i) # Numbers generated on-the-fly, not stored in memory
This makes Python 3’s range() both cleaner (one function, not two) and more memory-efficient out of the box.
Exception Syntax
A small but important syntax change that affects nearly every Python codebase.
Python 2 allowed both of these:
# Python 2
except ValueError, e: # Old syntax
except ValueError as e: # New syntax (also valid in Python 2.6+)
Python 3 only allows:
# Python 3
except ValueError as e:
print(e)
The old comma syntax is gone. This is a breaking change for any old code that used it, though you’d catch it immediately as a SyntaxError when running the code.
Dictionary Methods — Views Instead of Lists
In Python 2, dictionary methods like .keys(), .values(), and .items() return lists:
# Python 2
d = {"a": 1, "b": 2}
d.keys() # Returns ['a', 'b'] — a full list copy
In Python 3, they return view objects — dynamic, memory-efficient views of the dictionary’s contents:
# Python 3
d = {"a": 1, "b": 2}
d.keys() # Returns dict_keys(['a', 'b']) — a view, not a copy
This is more memory-efficient because no copy is made. The view always reflects the current state of the dictionary. If you actually need a list, just wrap it: list(d.keys()).
Type Hints and f-strings — Python 3 Exclusives
Two modern Python features that simply don’t exist in Python 2 and make code dramatically more readable:
f-strings (Python 3.6+):
# Python 2 — string formatting is clunky
name = "Priya"
print("Hello, %s! You are %d years old." % (name, 28))
# Python 3.6+ — clean and readable
print(f"Hello, {name}! You are {28} years old.")
You can see the output in the screenshot below.

Type hints (Python 3.5+):
# Python 3 — optional but incredibly useful
def add_numbers(a: int, b: int) -> int:
return a + b
Type hints don’t change how the code runs, but they make your code self-documenting and allow tools like mypy, pyright, and your IDE to catch type errors before you run anything. There’s nothing like this in Python 2.
What the Numbers Say About Adoption
As of 2026, Python 2 represents less than 2% of Python downloads on PyPI. Every major framework and library has dropped it:
- NumPy dropped Python 2 support in version 1.17 (2019)
- Matplotlib dropped Python 2 support in version 3.0 (2018)
- Pandas dropped Python 2 support in version 1.0 (2020)
- Django dropped Python 2 support in version 3.0 (2019)
- Flask dropped Python 2 support in 2.0 (2021)
If you’re trying to use any of these libraries today, and you almost certainly are, you need Python 3. There’s no way around it.
How to Check Which Python Version You’re Running
Open your terminal and run:
python --version
or
python3 --version
If you see Python 2.x anywhere, follow the upgrade steps below.
How to Upgrade From Python 2 to Python 3
If you’re maintaining a Python 2 codebase that needs to be migrated, here’s the practical approach:
Step 1: Install Python 3
Download the latest Python 3.x from python.org or install it via your package manager.
Step 2: Run the 2to3 tool
Python ships with a tool called 2to3 that automatically converts many Python 2 patterns to Python 3:
2to3 -w your_script.py
The -w flag writes the changes directly to the file (it creates a .bak backup first). It handles things like print statements, unicode literals, xrange, and more.
Step 3: Test thoroughly
2to3 catches the syntactic changes, but not the logic changes, especially around Unicode and string handling. Run your test suite and manually test anything that processes text, reads files, or handles user input.
Step 4: Update your dependencies
Run pip3 install -r requirements.txt to install the Python 3 compatible versions of your dependencies.
Step 5: Update your environment
Make sure any deployment scripts, Docker files, CI pipelines, and PATH settings point to Python 3, not Python 2.
The One Thing That Keeps People on Python 2
Almost every time I see someone still using Python 2, it’s for one of these reasons:
- Legacy code that “just works” — migrating feels risky and expensive
- A specific library or tool that hasn’t been updated (increasingly rare now)
- An older server or environment that IT hasn’t updated yet
If it’s the first reason, I understand. But running code with no security patches isn’t a stable equilibrium. The technical debt compounds, and the migration only gets harder the longer you wait.
If it’s the third reason, push for the upgrade. The argument is easy to make: “We’re running unsupported software with known CVEs.” That tends to get attention.
Should Beginners Even Think About Python 2?
No. If you’re learning Python today, start with Python 3.12 or 3.13. Don’t touch Python 2 unless you’re specifically maintaining legacy code and have no choice.
If you find a tutorial or course that uses Python 2 syntax, close the tab and find a current resource. There are excellent, free, up-to-date Python tutorials in 2026. There’s no reason to learn on a deprecated version.
Common Questions
Can Python 2 and Python 3 run on the same machine?
Yes. They install to different directories and use different executables (python2 vs python3 or python). Use virtual environments to keep projects isolated.
Will Python 2 code run on Python 3?
Some of it will, by coincidence. Most won’t, because of the syntax and behavioral changes covered above. Use 2to3 as a starting point for migration.
Is Python 3 faster than Python 3?
Python 3 is generally faster than Python 2, especially in recent versions. Python 3.11 and 3.12 introduced significant performance improvements — in some benchmarks, Python 3.12 is 60% faster than Python 3.10. Python 2 has seen no performance improvements since 2020.
I have a Python 2 package I rely on. What do I do?
Check if a Python 3 compatible fork exists on PyPI or GitHub. Search for python3 <package-name> — there’s usually a maintained fork or a direct replacement. If not, consider whether that dependency is worth carrying forward.
You may also read:
- Is Python a Good Language to Learn?
- What Is the Best Way to Learn Python?
- Should I Learn Java or Python?
- Should I Learn Python or C++?

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.