30 Simple Tricks to Instantly Level Up Your Python Skills
These bite-sized Python power-ups will help you write cleaner, faster, and more Pythonic code — whether you’re building scripts, APIs, or…

You’re not writing bad Python — but you could be writing brilliant Python.
30 Simple Tricks to Instantly Level Up Your Python Skills
These bite-sized Python power-ups will help you write cleaner, faster, and more Pythonic code — whether you’re building scripts, APIs, or full-blown apps.
Stop Writing Okay Python — Start Writing Great Python
Python is famously beginner-friendly — but the real fun starts when you move past the basics and begin to truly master the language.
These 30 simple but effective tricks are like cheat codes for writing smarter Python. They’re the tips I wish someone had handed me early on — and the ones I still reach for in production code, data pipelines, and side projects.
Some will help you write cleaner code. Others will save you lines. A few might even blow your mind a little.
1. Use List Comprehensions Like a Pro
A list comprehension is a concise way to create a list in Python. Instead of using a multi-line for
loop to build a list, you can do it in a single line.
It improves readability, performance, and makes your code look more Pythonic.
Traditional Way (Using a Loop)
Here’s the regular way you might generate a list of squares from numbers 0 through 9:
squares = []
for i in range(10):
squares.append(i ** 2)
That works just fine — but it’s 3 lines long, and a bit verbose.
Pythonic Way (Using List Comprehension)
Here’s the list comprehension version:
squares = [i ** 2 for i in range(10)]
That does exactly the same thing — in just one line. It’s shorter, clearer, and easier to understand at a glance.
Example with a Condition
Let’s say you only want even squares from 0 to 9:
even_squares = [i ** 2 for i in range(10) if i % 2 == 0]
This filters out the odd numbers before squaring them.
List comprehensions are one of the best tools in Python for clean, readable code. They’re great for:
Transforming lists
Filtering items
Performing simple calculations
Once you start using them, they’ll become second nature — and your code will look much more Pythonic.
2. Master enumerate()
Instead of Using Index Counters
The built-in function enumerate()
allows you to loop through a sequence (like a list or tuple) and automatically keep track of the index of each item.
It gives you both the index and the item in one go — cleanly and Pythonically.
Why Not Just Use a Manual Index?
Let’s say you have a list of items, and you want to print both the index and the item.
Old, Clunky Way (Manual Indexing):
items = ["apple", "banana", "cherry"]
i = 0
for item in items:
print(i, item)
i += 1
This works, but it’s repetitive.
You have to manually maintain the counter i
.
It’s prone to errors, especially in larger loops or nested ones.
The Pythonic Way: enumerate()
items = ["apple", "banana", "cherry"]
for i, item in enumerate(items):
print(i, item)
enumerate(items)
returns a tuple(index, item)
for each loop.
Cleaner, more readable, and less error-prone.
Python does the counting for you!
Want to Start from a Custom Index?
You can tell enumerate()
to start counting from any number using the start=
argument.
for i, item in enumerate(items, start=1):
print(f"{i}. {item}")
# ==== output =====
# 1. apple
# 2. banana
# 3. cherry
Use enumerate()
when looping if:
You need access to both the index and the value
You want to write code that’s easy to read and maintain
It’s one of those small Python tricks that makes a big difference.
3. Unpack Like a Pythonista
Unpacking allows you to assign multiple variables at once from a sequence (like a list or tuple). Instead of doing this manually:
person = ("Alice", 30, "Germany")
name = person[0]
age = person[1]
country = person[2]
You can do this in a single, elegant line:
name, age, country = ("Alice", 30, "Germany")
Python unpacks the values from the tuple and assigns them to variables based on position.
Common Examples of Unpacking
1. Tuple or List Unpacking
point = (3, 4)
x, y = point
2. Unpacking in a Loop
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
for number, letter in pairs:
print(f"{number} -> {letter}")
3. Ignoring Values with _
You can use _
as a throwaway variable for values you don’t need:
filename = "resume.pdf"
name, _ = filename.split(".")
# or
person = ("Alice", 30, "Germany")
name, _, _ = person # Only care about the name
4. Using *
to Grab “the Rest”
The *
operator lets you capture multiple items:
a, *b = [1, 2, 3, 4, 5]
print(a) # 1
print(b) # [2, 3, 4, 5]
# or
first, *middle, last = [10, 20, 30, 40, 50]
print(first) # 10
print(middle) # [20, 30, 40]
print(last) # 50
“Unpacking like a Pythonista” means using this elegant Python feature to:
Write less boilerplate
Avoid ugly indexing (x[0]
,x[1]
, etc.)
Work with iterables more naturally
Ignore or collect values flexibly
It’s simple, powerful, and very Pythonic.
4. Use zip()
to Loop Through Multiple Iterables
The built-in zip()
function combines two or more iterables (like lists or tuples) element-wise.
Think of it like a zipper — it “zips” elements together, side by side.
Basic Example
Suppose you have two lists:
names = ["Alice", "Bob", "Charlie"]
scores = [85, 90, 95]
If you want to print each name with its corresponding score, here’s the old-school way:
for i in range(len(names)):
print(names[i], scores[i])
Problems:
You manually manage indices
If the lists are not the same length, you can get errors
It’s not clean or readable
Pythonic Way with zip()
for name, score in zip(names, scores):
print(f"{name}: {score}")
What this does:
PairsAlice
with85
,Bob
with90
, and so on
You get a tuple for each pair: (name, score)
No manual indexing required
What Does zip()
Return?
zip()
returns a zip object, which is an iterator — you can loop over it or convert it into a list:
pairs = list(zip(names, scores))
print(pairs)
# Output: [('Alice', 85), ('Bob', 90), ('Charlie', 95)]
What Happens if the Lists Are Different Lengths?
zip()
stops at the shortest iterable:
names = ["Alice", "Bob"]
scores = [85, 90, 95]
for name, score in zip(names, scores):
print(name, score)
# Output:
# Alice 85
# Bob 90
zip()
is the go-to tool for looping over multiple iterables in parallel.
It’s cleaner and less error-prone than indexing.
Perfect for pairing up names and values, keys and data, etc.
5. Use *args
and **kwargs
to Write Flexible Functions
They are special syntax used in Python function definitions to accept a variable number of arguments.
*args
allows a function to accept any number of positional arguments (as a tuple).**kwargs
allows a function to accept any number of keyword arguments (as a dictionary).
In short: They make your functions dynamic and flexible.
Basic Example of *args
def greet(*names):
for name in names:
print(f"Hello, {name}!")
Now you can call greet()
with any number of names:
greet("Alice")
greet("Alice", "Bob", "Charlie")
What’s Happening?
*names
collects all extra positional arguments into a tuple.
Inside the function, you can loop through them like any list or tuple.
Basic Example of **kwargs
def print_info(**info):
for key, value in info.items():
print(f"{key}: {value}")
Now you can pass any keyword arguments:
print_info(name="Alice", age=30, country="Germany")
What’s Happening?
**info
collects all extra keyword arguments into a dictionary.
You can then loop through the keys and values.
Real-World Example: Combining Both
def greet_people(*names, **options):
for name in names:
greeting = options.get("greeting", "Hello")
emoji = options.get("emoji", "")
print(f"{greeting}, {name}! {emoji}")
# usage
greet_people("Alice", "Bob", greeting="Hi", emoji="👋")
# output :
# Hi, Alice! 👋
# Hi, Bob! 👋
Example: Passing Arguments to Another Function
def wrapper(func, *args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
def add(a, b):
return a + b
wrapper(add, 3, 5) # Output: 8
Bonus Tip: Argument Order Matters
When using all three types of parameters, the order should be:
def my_func(pos1, pos2, *args, kw1=None, kw2=None, **kwargs):
pass
*args
: Use when you don’t know how many positional arguments you'll get.
**kwargs
: Use when you don’t know how many named arguments you'll get.
Together, they help you write reusable, extensible, and dynamic functions.
6. Use get()
for Safe Dictionary Access
In Python, if you try to access a key in a dictionary that doesn’t exist, you’ll get a KeyError.
person = {"name": "Alice", "age": 30}
print(person["email"]) # ❌ KeyError: 'email'
That can crash your program if you’re not careful — especially when dealing with user input, JSON data, or optional fields.
Solution: Use .get()
for Safe Access
The get()
method lets you safely access a key in a dictionary without risking a crash if the key isn’t there.
email = person.get("email")
print(email) # ✅ None (instead of error)
No KeyError! If the key is missing, it simply returns None
(or whatever default value you specify).
Using .get()
on dictionaries is one of the safest and cleanest ways to access data when keys might be missing. It’s especially useful when:
Working with APIs or JSON data
Accessing optional fields
Writing robust, error-proof code
7. Replace If-Chains with Dictionaries
Sometimes, especially when mapping conditions to outcomes, we end up with bulky code like this:
def handle_status(code):
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
elif code == 500:
return "Server Error"
else:
return "Unknown"
It works, but:
It’s verbose
It can get messy as more conditions are added
It’s not as scalable or Pythonic
The Cleaner Way: Use a Dictionary Instead
You can simplify the same logic using a dictionary mapping:
def handle_status(code):
return {
200: "OK",
404: "Not Found",
500: "Server Error"
}.get(code, "Unknown")
What’s happening here?
Each status code is a key
Its corresponding message is the value
.get(code, "Unknown")
safely returns the result or"Unknown"
if the key doesn’t exist
Replacing if-elif-else
chains with dictionaries is a Pythonic pattern that helps when:
You’re mapping one value to another (e.g., status codes, commands, actions)
You want cleaner, more flexible, and less error-prone code
You value readability and scalability
8. Use F-Strings (They’re Fast & Clean)
F-strings (short for formatted string literals) were introduced in Python 3.6 and quickly became the preferred way to insert variables into strings.
They are:
Fast (faster than older formatting methods)
Clean (very readable)
Flexible (can include expressions, not just variables)
Basic Example
Let’s say you have this:
name = "Alice"
age = 30
Old way (using .format()
):
print("My name is {} and I am {} years old.".format(name, age))
F-string way:
print(f"My name is {name} and I am {age} years old.")
Much cleaner, right?
You Can Do More Than Just Insert Variables
1. Inline Calculations
price = 49.99
quantity = 3
print(f"Total: ${price * quantity}")
# Output: Total: $149.97
2. Call Functions
def shout(word):
return word.upper()
print(f"Attention: {shout('hello')}")
# Output: Attention: HELLO
3. Formatting Numbers
pi = 3.14159265
print(f"Pi rounded to 2 decimals: {pi:.2f}")
# Output: Pi rounded to 2 decimals: 3.14
4. Add Padding / Alignment
for i in range(1, 4):
print(f"{i:<5} | {i**2:>5}")
Use them whenever you need to include values or expressions inside a string — logging, reporting, displaying results, debugging, etc.
Bonus: Debugging with F-Strings (Python 3.8+)
user = "Alice"
print(f"{user=}")
# Output: user='Alice'
This prints both the variable name and value — super handy for debugging!
F-strings are:
Clean: No clunky formatting syntax
Fast: The most efficient string formatting method
Smart: They support expressions, function calls, and formatting options
They’re now the standard way to format strings in Python — if you’re not using them yet, now’s the time to start.
9. Leverage any()
and all()
for Clean Checks
These two functions are used to test collections of conditions, like lists, tuples, or generator expressions.
any(iterable)
Returns True
if at least one element in the iterable is True
.
all(iterable)
Returns True
only if all elements in the iterable are True
.
They’re perfect for cleaning up complex if
statements, especially when you need to check multiple conditions.
Real-Life Analogy
any()
is like asking:
“Is any light in the house turned on?” →True
if even one light is on
all()
is like asking:
“Are all the doors locked?” →True
only if every single door is locked
Example 1: Using any()
to Check for a Digit in a Password
password = "hello123"
if any(char.isdigit() for char in password):
print("Password contains at least one number.")
No need to loop manually — No need for flags or early returns
Example 2: Using all()
to Validate a Form
fields = ["Alice", "alice@email.com", "Germany"]
if all(fields):
print("All fields are filled out.")
else:
print("Please fill in all fields.")
Here, all()
checks that none of the fields are empty strings (which are considered False
in Python).
More Examples
Clean up multiple boolean checks
conditions = [user.is_active, user.is_verified, user.has_accepted_terms]
if all(conditions):
print("User is fully onboarded")
Check if any word in a sentence is uppercase
words = "this is GREAT".split()
if any(word.isupper() for word in words):
print("There's shouting in the text 😬")
any()
returnsTrue
if at least one item isTrue
all()
returnsTrue
if every item isTrue
They work with lists, tuples, sets, or generator expressions
They simplify your code and make your logic more readable
10. Use Generators for Memory-Efficient Iteration
A generator is a special kind of iterable in Python that yields values one at a time, on the fly, as needed — instead of computing and storing everything in memory upfront.
You can think of a generator like a lazy iterator:
It doesn’t generate the entire result all at once.
It pauses after each value is produced, and resumes when the next is requested.
Traditional List (Eager Evaluation)
def get_numbers():
return [i for i in range(1_000_000)]
This builds a list of 1 million numbers in memory all at once.
That can be slow and memory-hungry.
Generator (Lazy Evaluation)
def get_numbers():
for i in range(1_000_000):
yield i
Now:
nums = get_numbers() # No numbers created yet!
for n in nums:
print(n) # Numbers are generated one at a time
Nothing is stored in memory.
Each value is produced only when needed.
How to Create a Generator
There are two main ways:
1. With yield
in a function (like above)
def countdown(n):
while n > 0:
yield n
n -= 1
2. With a Generator Expression (like a list comprehension, but with ()
)
squares = (x**2 for x in range(1000000)) # Not a list!
for s in squares:
if s > 100:
break
print(s)
Generators are one of the most powerful tools in Python for writing efficient, scalable programs:
yield
= turn a function into a generator
Generator expressions = like list comprehensions, but lazy
Use them whenever you need to stream data, avoid memory spikes, or delay computation
11. Use set()
to remove duplicates
A set
is a built-in unordered collection of unique elements. That means:
No duplicates allowed
Items can be added, removed, or checked for existence
Very fast lookup (like a dictionary)
Here’s a quick example:
my_list = [1, 2, 2, 3, 4, 4, 5]
unique = set(my_list)
print(unique) # Output: {1, 2, 3, 4, 5}
Boom — duplicates gone.
Basic Example
emails = [
"a@example.com",
"b@example.com",
"a@example.com", # duplicate
]
unique_emails = list(set(emails))
print(unique_emails)
Order is not preserved — unless you take extra steps (explained below).
Want to Remove Duplicates but Keep Order?
Since sets are unordered, wrapping a list in set()
will scramble the order. If order matters, you can use this trick:
def dedupe_keep_order(seq):
seen = set()
return [x for x in seq if not (x in seen or seen.add(x))]
names = ["Alice", "Bob", "Alice", "Charlie", "Bob"]
print(dedupe_keep_order(names))
# Output: ['Alice', 'Bob', 'Charlie']
set()
removes duplicates — instantly and clearly
But it doesn’t preserve order
Use a custom function if you need to keep the order
This is one of those Python tricks that’s dead simple, but wildly useful once you get the hang of it.
12. Chain comparisons cleanly
This is one of Python’s most elegant features — and once you understand it, you’ll never want to write long comparisons the old way again.
In many programming languages (like Java, C, or JavaScript), if you want to check if a value lies between two numbers, you’d write:
if (10 < x && x < 20)
But in Python, you can write that more naturally, just like math:
if 10 < x < 20:
It reads exactly like you’d say it:
“If x is greater than 10 and less than 20”
How It Works
Python allows comparison chaining, so expressions like this:
a < b < c
Are interpreted as:
a < b and b < c
Python evaluates it in logical order, but more efficiently (only once per variable).
13. Swap variables without temp
In many programming languages, swapping two variables requires a temporary third variable:
temp = a
a = b
b = temp
But in Python, there’s a cleaner, faster, and more Pythonic way:
a, b = b, a
Why It Works
Python supports tuple unpacking, which lets you assign multiple variables in a single line.
Here’s what happens:
a, b = b, a
- The right-hand side creates a tuple:
(b, a)
- Python unpacks that tuple into the variables on the left:
a, b
Example:
x = 5
y = 10
x, y = y, x
print(x) # 10
print(y) # 5
Imagine you’re sorting a list manually:
if numbers[i] > numbers[i + 1]:
numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i]
Perfect for quick swaps — no temp clutter needed.
14. Use defaultdict
from collections
A defaultdict
is a special type of dictionary from Python’s collections
module that automatically assigns default values to keys that don’t exist — so you don’t get a KeyError
.
The Problem With Normal Dicts
Let’s say you’re trying to count the number of times each letter appears in a string:
text = "banana"
letter_counts = {}
for char in text:
if char in letter_counts:
letter_counts[char] += 1
else:
letter_counts[char] = 1
This works — but it’s verbose.
The defaultdict
Way
Here’s the cleaner, Pythonic version using defaultdict
:
from collections import defaultdict
text = "banana"
letter_counts = defaultdict(int)
for char in text:
letter_counts[char] += 1
print(letter_counts)
Boom! No need to check if the key exists. defaultdict
automatically initializes it to 0
(in this case) when missing.
How It Works
When you create a defaultdict
, you pass in a factory function — like int
, list
, set
, etc.
Here’s what each does:
defaultdict(int)
→ missing keys start with0
defaultdict(list)
→ missing keys start as empty[]
defaultdict(set)
→ missing keys start as emptyset()
Example with list
:
from collections import defaultdict
grouped = defaultdict(list)
data = [
("fruit", "apple"),
("fruit", "banana"),
("vegetable", "carrot")
]
for category, item in data:
grouped[category].append(item)
print(grouped)
Output
defaultdict(<class 'list'>, {
'fruit': ['apple', 'banana'],
'vegetable': ['carrot']
})
defaultdict
avoidsKeyError
by providing automatic default values.
Cleaner and faster than manualif...else
ordict.get()
.
Super useful for counting, grouping, and initializing collections.
15. Use Counter
for frequency
Counter
is a powerful class from Python’s collections
module designed specifically to count the frequency of elements in an iterable — like a list, string, or tuple.
It’s like a dictionary, but optimized for counting.
The Old Way
Let’s say you want to count how many times each letter appears in a word:
word = "mississippi"
counts = {}
for letter in word:
if letter in counts:
counts[letter] += 1
else:
counts[letter] = 1
print(counts)
It works, but it’s verbose and repetitive.
The Counter
Way (Pythonic)
from collections import Counter
word = "mississippi"
counts = Counter(word)
print(counts)
# output : Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
Much cleaner — and faster too!
Why It’s Powerful
The Counter
object is more than a simple dictionary. It comes with super useful methods:
1. .most_common(n)
Returns the top n
most common elements:
counts.most_common(2)
# [('i', 4), ('s', 4)]
2. Arithmetic Operations
You can add, subtract, and intersect multiple Counters:
c1 = Counter("apple")
c2 = Counter("plea")
print(c1 + c2) # Adds frequencies
print(c1 - c2) # Subtracts frequencies
print(c1 & c2) # Intersection (min of counts)
print(c1 | c2) # Union (max of counts)
Counter
simplifies frequency counting to one line.
Comes with useful methods like .most_common()
.
Supports arithmetic and set-like operations.
Great for strings, lists, logs, and analytics.
16. Sort with custom keys
Python’s built-in sorted()
function and .sort()
method allow you to sort complex data — not just numbers or strings — using a custom rule, via the key=
parameter.
Think of it as telling Python:
“Hey, sort this based on this logic instead of default comparison.”
The Basics
numbers = [5, 2, 9, 1]
sorted_numbers = sorted(numbers) # Default sort: ascending
But what if you want to sort…
A list of strings by their length?
A list of dictionaries by a specific key?
A list of tuples by the second item?
That’s where key=
comes in.
Example 1: Sort strings by length
words = ["banana", "pie", "Washington", "to"]
sorted_words = sorted(words, key=len)
print(sorted_words)
# output : ['to', 'pie', 'banana', 'Washington']
Here, key=len
tells Python to use the length of each word for sorting.
Example 2: Sort tuples by the second item
pairs = [(1, 3), (2, 2), (4, 1)]
sorted_pairs = sorted(pairs, key=lambda x: x[1])
print(sorted_pairs)
# output : [(4, 1), (2, 2), (1, 3)]
You’re saying:
“Sort each tuple by its second element (x[1]
)."
Example 3: Sort dictionaries by a value
students = [
{"name": "Alice", "score": 85},
{"name": "Bob", "score": 92},
{"name": "Charlie", "score": 78}
]
sorted_students = sorted(students, key=lambda s: s["score"], reverse=True)
Output (sorted by score descending):
[
{'name': 'Bob', 'score': 92},
{'name': 'Alice', 'score': 85},
{'name': 'Charlie', 'score': 78}
]
Use operator
module
If you’re sorting by dict key or object attribute, use itemgetter
or attrgetter
for cleaner code.
from operator import itemgetter
sorted_students = sorted(students, key=itemgetter("score"))
17. Use Pathlib
over os.path
pathlib
is a modern, object-oriented library in Python for working with filesystem paths. It was introduced in Python 3.4 and is designed to replace the older os.path
module, which uses string manipulation to work with file and directory paths.
With pathlib
, paths are no longer plain strings — they become Path objects, which makes your code cleaner, more readable, and cross-platform.
The Old Way (Using os.path
)
import os
path = "/home/user"
file = "notes.txt"
full_path = os.path.join(path, file)
if os.path.exists(full_path):
print("File exists!")
It works, but it’s string-heavy and not very intuitive.
The Modern Way (Using pathlib
)
from pathlib import Path
path = Path("/home/user") / "notes.txt"
if path.exists():
print("File exists!")
Notice how much cleaner and natural the syntax is?
You’re chaining paths using the /
operator instead of os.path.join()
.
Common pathlib
Examples
Create a path
p = Path("folder") / "subfolder" / "file.txt"
Check if file exists
if p.exists():
print("It exists")
Create directories
p.mkdir(parents=True, exist_ok=True)
Read a file
content = p.read_text()
Write to a file
p.write_text("Hello, world!")
Iterate through a directory
for file in Path("my_folder").iterdir():
print(file.name)
Find all .txt
files recursively
for file in Path("docs").rglob("*.txt"):
print(file)
18. Use context managers for safety
A context manager in Python is a construct that properly manages resources — like files, network connections, database sessions, or locks — ensuring they’re cleaned up properly (even if errors occur).
You usually use them via the with
statement.
The Problem Without Context Managers
Let’s say you open a file like this:
file = open("data.txt", "r")
data = file.read()
file.close()
Looks fine, but what if an error happens between open()
and close()
?
The file may never get closed, causing memory leaks, locked files, or other issues.
The Context Manager Way
with open("data.txt", "r") as file:
data = file.read()
Once the code inside the with
block finishes (or errors out), Python automatically closes the file for you.
It’s cleaner, safer, and more Pythonic.
More Real-World Examples
Working with Files
with open("log.txt", "w") as f:
f.write("Logging started")
Using Threading Locks
from threading import Lock
lock = Lock()
with lock:
# safely access shared resource
do_something()
Temporary Files
import tempfile
with tempfile.TemporaryFile() as tmp:
tmp.write(b"Hello")
SQLite Database Connections
import sqlite3
with sqlite3.connect("mydb.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
The connection will automatically commit and close when the block exits.
You Can Make Your Own Context Managers Too
Using the contextlib
module:
from contextlib import contextmanager
@contextmanager
def open_file(path):
f = open(path, 'r')
try:
yield f
finally:
f.close()
# Now use it like:
with open_file("data.txt") as f:
print(f.read())
19. Use type hints
for clarity
Type hints (also called type annotations) were introduced in Python 3.5 via PEP 484.
They allow you to explicitly specify the expected data types of variables, function arguments, and return values — but without enforcing them at runtime.
They’re mainly for:
Clarity
IDE support
Better collaboration
Static type checking (using tools likemypy
orpyright
)
Without Type Hints
def greet(name):
return "Hello, " + name
This works, but it’s unclear what type name
should be — a string? number?
With Type Hints
def greet(name: str) -> str:
return "Hello, " + name
Now it’s obvious:
name
should be a string
The function returns a string
This improves readability and helps catch bugs before runtime.
IDEs Love Type Hints
When you use hints, tools like VSCode, PyCharm, or even Copilot can:
Autocomplete arguments
Warn about type mismatches
Suggest better code
More Examples
Function with multiple argument types:
def add(x: int, y: int) -> int:
return x + y
Optional / Default values:
from typing import Optional
def get_user(id: int) -> Optional[str]:
# returns None or a username
...
Lists and Dictionaries:
from typing import List, Dict
def square_all(numbers: List[int]) -> List[int]:
return [n ** 2 for n in numbers]
def get_usernames(users: Dict[int, str]) -> List[str]:
return list(users.values())
Custom Classes:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
Static Type Checking with mypy
You can run:
mypy your_script.py
And get early warnings if types don’t match. It’s like a spellchecker for your logic.
20. Use __slots__
in classes to save memory
In Python, when you define a regular class, Python stores all instance attributes in a special dictionary called __dict__
.
This allows you to dynamically add new attributes at runtime, but it uses extra memory.
By defining __slots__
, you tell Python exactly which attributes your objects will have, and Python doesn’t create a __dict__
for each object — saving memory.
The Problem with Regular Classes
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
Each Person
object here has a __dict__
, which stores its attributes like this:
person.__dict__ # {'name': 'Alice', 'age': 30}
This __dict__
is flexible, but takes extra space per object, especially if you're creating millions of instances (e.g., in data processing, simulations, etc.).
The __slots__
Solution
class Person:
__slots__ = ['name', 'age'] # only these two attributes allowed
def __init__(self, name, age):
self.name = name
self.age = age
Now:
No __dict__
is created.
Memory usage is reduced.
You can’t dynamically add new attributes (which is a good thing in many cases — it enforces structure).
Try this:
p = Person("Alice", 30)
p.name # works
p.new_attr = "Oops" # ❌ AttributeError: 'Person' object has no attribute 'new_attr'
When Should You Use __slots__
?
Use it when:
You have many instances of a class (think: thousands or millions).
Your objects don’t need dynamic attributes.
You care about memory optimization and speed.
How Much Memory Does It Save?
Let’s compare:
import sys
class Regular:
def __init__(self, x, y):
self.x = x
self.y = y
class Slotted:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
r = Regular(1, 2)
s = Slotted(1, 2)
print(sys.getsizeof(r)) # might be ~56-64 bytes + overhead
print(sys.getsizeof(s)) # usually smaller
The actual savings depend on your Python version and platform, but with many objects, the gain adds up.
21. Use dataclasses
for clean models
Python’s dataclasses
module (introduced in Python 3.7) provides a clean, concise way to write classes that are primarily used to store data — like models or records — without all the boilerplate code.
Instead of manually writing __init__
, __repr__
, __eq__
, and other methods, you just use a decorator: @dataclass
.
Traditional (Verbose) Way
Let’s say you want a simple Book
class:
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __repr__(self):
return f"Book(title={self.title}, author={self.author}, price={self.price})"
def __eq__(self, other):
return (self.title == other.title and
self.author == other.author and
self.price == other.price)
That’s a lot of code just for a container of data.
Now With @dataclass
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
price: float
That’s it.
Python automatically gives you:
__init__
__repr__
__eq__
Optionally: ordering, immutability, defaults, and more
Usage Example
book1 = Book("Python 101", "Aashish Kumar", 9.99)
book2 = Book("Python 101", "Aashish Kumar", 9.99)
print(book1) # Book(title='Python 101', author='Aashish Kumar', price=9.99)
print(book1 == book2) # True
You get readable output and comparison for free!
22. Inline if
statements
An inline if
statement (also called a ternary operator) lets you write short, one-line conditional expressions.
Instead of using a full if...else
block, you can write the logic in a single, readable line.
Traditional if...else
:
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"
Inline if
Version:
status = "Adult" if age >= 18 else "Minor"
Same logic — but way cleaner for simple conditions.
Real-World Examples:
Set a label based on a score:
label = "Pass" if score >= 40 else "Fail"
Choose an icon based on a flag:
icon = "🔒" if is_private else "🌐"
Format output:
print(f"Hello, {name if name else 'Guest'}")
Be Careful
Avoid overusing inline if
for complex logic — it can hurt readability.
Bad:
result = "win" if score > 80 else "average" if score > 50 else "lose"
Better:
Use a regular if...elif...else
for clarity.
23. Use try/except
smartly — not excessively
In Python, try/except
blocks are used to handle errors gracefully. But overusing them or using them the wrong way can hide bugs and make your code harder to understand and debug.
The trick is to use try/except
intelligently — only where it’s necessary and expected, not to wrap huge blocks or ignore errors silently.
Smart try/except
Usage
Let’s say you’re converting user input to a number:
try:
age = int(user_input)
except ValueError:
print("Please enter a valid number.")
This is smart:
You expect that input might not be a number.
You’re only catching the specific error (ValueError
).
You handle it cleanly.
Bad Example — Too Broad:
try:
# A lot of things happening
do_this()
do_that()
maybe_fail()
except:
pass # silently ignoring ALL errors
Problems here:
Catches all exceptions, even serious ones likeKeyboardInterrupt
,MemoryError
, or bugs in logic.
The error is hidden — which makes debugging a nightmare.
There’s no logging, fallback, or correction.
Smart Tips for Using try/except
1. Catch only what you need
Don’t use a bare except:
— it swallows everything.
try:
result = 10 / x
except ZeroDivisionError:
result = 0
2. Keep it short
Wrap only the specific line that might raise an error:
# Bad
try:
# this line doesn't raise
print("Hello")
risky_function() # only this can fail
except SomeError:
print("Something went wrong")
# Good
print("Hello")
try:
risky_function()
except SomeError:
print("Something went wrong")
3. Avoid using it for flow control
Some developers do this (don’t):
try:
my_dict["key"]
except KeyError:
my_dict["key"] = "default"
Instead, use:
my_dict.setdefault("key", "default")
Or:
if "key" not in my_dict:
my_dict["key"] = "default"
Exceptions are for exceptional situations, not for regular logic.
4. Use else
and finally
if needed
try:
file = open("data.txt")
except FileNotFoundError:
print("File not found.")
else:
data = file.read()
print(data)
finally:
file.close()
else
: runs only if no exception occurred
finally
: runs no matter what (great for cleanup)
24. Combine multiple strings with .join()
Instead of combining strings with +
or using loops, Python gives you a fast, readable, and efficient way to concatenate strings using:
"separator".join(iterable)
Example:
words = ["Python", "is", "awesome"]
sentence = " ".join(words)
print(sentence) # Output: Python is awesome
Why .join()
is Better than +
Let’s say you try this:
sentence = ""
for word in words:
sentence += word + " "
This works — but it’s inefficient, especially with long lists, because each +
creates a new string in memory. Strings in Python are immutable, so every time you add one, Python builds a brand new one.
Using .join()
is faster and more memory-efficient, because it allocates the memory just once.
How .join()
Works
It takes an iterable (like a list or tuple)
Places the string (the one you call .join()
on) between each element
More Examples:
csv = ",".join(["apple", "banana", "cherry"])
print(csv) # apple,banana,cherry
path = "/".join(["home", "user", "documents"])
print(path) # home/user/documents
hyphenated = "-".join(["2025", "08", "07"])
print(hyphenated) # 2025-08-07
Bonus: Avoid This Common Mistake
Some try to call .join()
like this:
words.join(" ") # ❌ This will throw an error
Correct syntax:
" ".join(words) # ✅
The string you’re using as a separator (like " "
or "-"
) must come before .join()
— and the list goes inside the parentheses.
Use .join()
whenever you're combining strings in a loop or list. It’s the Pythonic way — and it’s faster, cleaner, and safer.
25. Use repr()
for debug-friendly strings
repr()
stands for representation. It returns a string that shows how an object can be recreated, or gives a detailed internal view of the object — ideal for debugging.
Compare it to str()
:
str()
is meant for human-friendly output.
repr()
is meant for developer-friendly output.
Quick Example:
name = "Aashish"
print(str(name)) # Aashish
print(repr(name)) # 'Aashish'
See how repr()
includes the quotes? That’s helpful to see exact formatting, escape characters, or trailing spaces.
When Should You Use repr()
?
- Debugging: See raw or unambiguous values (e.g., strings with newline characters or whitespace)
- Logging: Want to see values as they exist in code.
- Creating string representations of objects for developers (via
__repr__()
method in classes).
Real-World Example
text = "Hello\nWorld"
print(str(text)) # Hello
# World
print(repr(text)) # 'Hello\nWorld'
With repr()
, you clearly see the \n
newline character — very helpful when debugging weird string bugs.
Bonus: Custom __repr__()
in Your Own Classes
If you create custom classes, you can define how they appear when printed using repr()
:
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={repr(self.name)})"
user = User("Aashish")
print(user) # User(name='Aashish')
This is much better than the default <__main__.User object at 0xABC123>
.
Use repr()
when logging or debugging anything suspicious — like:
print(f"User input: {repr(user_input)}")
This way, you’ll spot unwanted \n
, \t
, spaces, etc.
26. Use isinstance()
over type()
When checking if a variable is a certain type, Python gives you two options:
type(x) == Something
isinstance(x, Something)
(recommended)
Though both seem to work, isinstance()
is more powerful, flexible, and Pythonic.
Why isinstance()
is Better
Here’s a side-by-side comparison:
x = 5
# Using type
if type(x) == int:
print("x is an integer")
# Using isinstance
if isinstance(x, int):
print("x is an integer")
Both work. So what’s the difference?
Let’s say you’re working with inheritance:
class Animal:
pass
class Dog(Animal):
pass
d = Dog()
Now check:
type(d) == Animal # ❌ False
isinstance(d, Animal) # ✅ True
isinstance()
checks if the object is an instance of a class or its subclasses.
type()
checks for exact type match only — it does not consider inheritance.
Real-World Use Case
Let’s say you’re writing a function that can accept either an integer or a float:
def add_ten(x):
if isinstance(x, (int, float)):
return x + 10
raise TypeError("x must be a number")
Here, you’re checking for multiple types — which type()
can’t do directly. But isinstance()
makes it simple.
If you ever need to check if a variable is any of several types, just pass a tuple:
isinstance(x, (int, float, complex))
Use isinstance()
instead of type()
— especially when dealing with inheritance, polymorphism, or multiple types. It’s cleaner, safer, and just more Pythonic.
27. Use itertools
for advanced iteration
itertools
is a standard library module in Python that provides a collection of fast, memory-efficient tools for working with iterators.
It’s like a Swiss Army knife for looping — letting you chain, group, filter, and repeat iterables in ways that are both powerful and elegant.
Why Use itertools
?
Handles large datasets without memory bloat
Offers lazy evaluation (computes on-the-fly)
Provides cleaner, more readable code
Ideal for complex looping patterns like combinations, groupings, or infinite sequences
Most Useful itertools
Functions
Let’s look at some practical examples you’ll actually use.
1. itertools.count(start=0, step=1)
Creates an infinite counter — useful for indexing or generating IDs.
from itertools import count
for i in count(10, 2):
print(i)
if i > 20:
break
Output:
10
12
14
16
18
20
22
2. itertools.cycle(iterable)
Endless loop over an iterable — great for round-robin scheduling or repeating behavior.
from itertools import cycle
colors = ['red', 'green', 'blue']
cycler = cycle(colors)
for _ in range(5):
print(next(cycler))
Output:
red
green
blue
red
green
3. itertools.chain(*iterables)
Flattens multiple iterables into a single stream.
from itertools import chain
a = [1, 2, 3]
b = [4, 5]
print(list(chain(a, b)))
Output:
[1, 2, 3, 4, 5]
Great when merging multiple lists without nesting.
4. itertools.combinations(iterable, r)
All possible r-length combinations (no repeats).
from itertools import combinations
items = ['a', 'b', 'c']
print(list(combinations(items, 2)))
Output:
[('a', 'b'), ('a', 'c'), ('b', 'c')]
Perfect for generating test cases or solving algorithm challenges.
5. itertools.groupby(iterable, key=...)
Groups adjacent items with the same key — commonly used with sorted data.
from itertools import groupby
data = ['a', 'a', 'b', 'b', 'c', 'a']
for key, group in groupby(data):
print(key, list(group))
Output:
a ['a', 'a']
b ['b', 'b']
c ['c']
a ['a']
Note: Works only on consecutive groups — so data often needs to be pre-sorted.
Advanced Tip: Combine itertools
with map()
, filter()
, or generators for powerful data pipelines.
Example:
from itertools import islice
# Infinite counter, sliced down to just 5 items
print(list(islice(count(100, 5), 5)))
# output : [100, 105, 110, 115, 120]
Use itertools
when your loop logic starts to feel clunky — it can save you lines of code and tons of memory.
28. Chain filters and maps
Instead of writing multiple for-loops or cluttered list comprehensions, you can chain Python’s built-in filter()
and map()
functions to build powerful, readable, and memory-efficient pipelines.
This trick is especially useful when dealing with data cleaning, transformation, or extraction.
Let’s Understand the Two:
filter(function, iterable)
: Filters out elements where the function returnsFalse
.
map(function, iterable)
: Applies a function to each element in an iterable and returns a new iterable.
Chaining Them — Real Example
Goal: From a list of numbers, filter out odd numbers and then square the remaining even numbers.
numbers = [1, 2, 3, 4, 5, 6]
result = map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))
print(list(result))
# output : [4, 16, 36]
What’s Happening Here?
filter(lambda x: x % 2 == 0, numbers)
keeps only even numbers →[2, 4, 6]
map(lambda x: x**2, ...)
squares each →[4, 16, 36]
Chaining filter()
and map()
makes your code:
More expressive
Easier to test
More efficient
And Python is all about writing elegant, powerful one-liners that still remain readable.
29. Use timeit
to benchmark code
Python’s built-in timeit
module is the most accurate way to measure the execution time of small bits of code.
It’s far better than using time.time()
because it:
Automatically runs your code multiple times for better accuracy
Minimizes the effect of background processes or caching
Is cross-platform consistent for benchmarking
Simple Example: Comparing Two Ways to Create a List
Let’s compare two ways to create a list of numbers from 0 to 999:
# Method 1: List comprehension
timeit.timeit('[x for x in range(1000)]', number=1000)
# Method 2: Using list() and range()
timeit.timeit('list(range(1000))', number=1000)
This runs each snippet 1,000 times and tells you how long it took.
Full Example with timeit
import timeit
stmt1 = '[x for x in range(1000)]'
stmt2 = 'list(range(1000))'
time1 = timeit.timeit(stmt1, number=1000)
time2 = timeit.timeit(stmt2, number=1000)
print(f"List comprehension: {time1:.4f} sec")
print(f"list(range()): {time2:.4f} sec")
Output (will vary per system):
List comprehension: 0.0524 sec
list(range()): 0.0289 sec
This tells you that list(range())
is slightly faster in this case.
Why Not Use time.time()
Instead?
import time
start = time.time()
# do stuff
end = time.time()
print(end - start)
This method:
Is OK for long-running tasks
Is less accurate for small functions or fast code
Can be affected by system clock updates or lag
Use Case: Optimize Code with Real Data
Say you’re optimizing a function:
def slow_way():
return sorted(set([x for x in range(10000)]))
def fast_way():
return list(dict.fromkeys(range(10000)))
Use timeit
to compare:
import timeit
print(timeit.timeit('slow_way()', globals=globals(), number=100))
print(timeit.timeit('fast_way()', globals=globals(), number=100))
If you’re serious about writing performant Python code, you should be using timeit
— it helps you understand what’s truly faster, not just what looks cleaner.
30. Use dir()
and help()
often to explore
Python is known for its introspective capabilities — meaning, you can examine objects, modules, or even built-in types at runtime to discover what they can do. Two built-in functions that help with this are:
dir()
→ Lists all attributes and methods of an object
help()
→ Provides a human-readable description/documentation
These are especially helpful when:
You’re working with new libraries or unfamiliar objects
You want to quickly discover methods without Googling
You’re in a REPL or using Jupyter Notebook/IPython
dir()
– See What’s Inside
Think of dir()
as a way to peek under the hood.
x = [1, 2, 3]
print(dir(x))
Output:
['__add__', '__class__', ..., 'append', 'clear', 'copy', 'count', 'extend', ...]
You’ll see everything that the list object x
can do — methods like .append()
, .clear()
, .copy()
, etc.
You can also run:
import math
print(dir(math))
To list all functions and constants in the math
module.
help()
– Get Documentation Instantly
If dir()
is a map, help()
is the guidebook.
Example:
help(list.append)
Output:
Help on method_descriptor:
append(self, object, /)
Append object to the end of the list.
Boom — now you know what it does and how to use it. No Stack Overflow required.
You can also use help()
on entire modules:
import math
help(math)
Or on your own functions:
def greet(name):
"""Greets the person by name."""
print(f"Hello, {name}!")
help(greet)
Python will read the docstring and show you what your function does. This is great when collaborating or revisiting your own code later.
The best Python developers explore constantly. Using dir()
and help()
often will level up your understanding and help you become self-sufficient, curious, and faster at solving problems.
Final Thoughts: The Difference Is in the Details
Mastering Python isn’t about memorizing every method in the standard library. It’s about writing better code — cleaner, faster, more elegant solutions that save you time and reduce bugs.
These 30 tips won’t just make your code shorter — they’ll change the way you think in Python.
Pick 3–5 and start using them today. Bookmark this list. Return when you’re ready to level up again.
Keep leveling up. The Pythonic way is the smart way.
