Absolutely — here's the mirrored and expanded version of the Python best practices list. Each recommendation now includes a clear explanation of why it's preferred, and I've continued with more tips that rely only on base Python features.
Use:
if x is None:Instead of:
if x == None:Why: None is a singleton object in Python, so identity comparison (is) is faster and more accurate than value comparison (==). It avoids unexpected behavior if x defines a custom __eq__ method.
Use:
if my_list:Instead of:
if len(my_list) > 0:Why: Python treats empty containers and zero-like values as False (examples: None, False, 0, '', [], {}, set()). This makes conditions more concise and expressive, and avoids unnecessary function calls like len().
Use:
squares = [x * x for x in range(10)]Instead of:
squares = []
for x in range(10):
squares.append(x * x)Why: List comprehensions are more readable, often faster, and reduce boilerplate code. They’re ideal for transforming or filtering data in a single line.
Use:
for i, value in enumerate(my_list):
print(i, value)Instead of:
for i in range(len(my_list)):
print(i, my_list[i])Why: enumerate() is cleaner and avoids off-by-one errors. It also communicates intent more clearly when you need both the index and the value.
Use:
with open('file.txt') as f:
data = f.read()Instead of:
f = open('file.txt')
data = f.read()
f.close()Why: The with statement ensures that resources like files are properly closed, even if an error occurs. It’s safer and more idiomatic.
Use:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsInstead of:
def add_item(item, items=[]):
items.append(item)
return itemsWhy: Mutable default arguments persist across function calls, which can lead to unexpected behavior. Using None avoids shared state bugs.
Use:
try:
value = my_dict['key']
except KeyError:
value = defaultInstead of:
if 'key' in my_dict:
value = my_dict['key']
else:
value = defaultWhy: EAFP is more idiomatic in Python and often more performant, especially when the check is expensive or the failure is rare.
Use:
name = "Alice"
print(f"Hello, {name}!")Instead of:
print("Hello, " + name + "!")Why: f-strings are faster, more readable, and allow inline expressions. They’re the modern standard for string formatting in Python 3.6+.
Use:
print(f"{{username}} is not the same as {username}")Instead of:
print(f"{username} is not the same as {}") # ❌ SyntaxErrorWhy: In f-strings, {} is used for expressions. To include literal braces, you must double them ({{ and }}).
Use:
for name, score in zip(names, scores):
print(f"{name}: {score}")Instead of:
for i in range(len(names)):
print(f"{names[i]}: {scores[i]}")Why: zip() is cleaner and avoids index mismatches. It’s ideal when you want to pair elements from multiple sequences.
Use:
if any(x > 10 for x in numbers):
print("At least one number is greater than 10")Instead of:
found = False
for x in numbers:
if x > 10:
found = True
break
if found:
print("At least one number is greater than 10")Why: any() and all() are built-in functions that simplify logic and improve readability for condition checks across iterables.
Use:
if is_valid:Instead of:
if is_valid == True:Why: Boolean expressions already evaluate to True or False. Comparing them again is redundant and less readable.
Use:
q, r = divmod(10, 3)Instead of:
q = 10 // 3
r = 10 % 3Why: divmod() is more efficient and expressive when you need both the quotient and remainder.
Use:
if item in my_set:Instead of:
if item in my_list:Why: Sets offer constant-time membership tests, while lists require linear-time searches. Use sets when order doesn’t matter and performance does.
Use:
a, b = b, aInstead of:
temp = a
a = b
b = tempWhy: Tuple unpacking is more concise and avoids temporary variables. It’s a Pythonic way to swap values.
Use:
value = my_dict.get('key', default)Instead of:
if 'key' in my_dict:
value = my_dict['key']
else:
value = defaultWhy: get() simplifies access and avoids verbose conditional logic. It’s ideal for optional keys.
Use:
sentence = ' '.join(words)Instead of:
sentence = ''
for word in words:
sentence += word + ' 'Why: join() is faster and avoids repeated string allocations. It’s the preferred way to build strings from lists.
Use:
for x in sorted(my_list, reverse=True):
print(x)Instead of:
my_list.sort()
my_list.reverse()
for x in my_list:
print(x)Why: sorted() and reversed() return new iterables and don’t mutate the original list. They’re safer and more flexible.
Use:
total = sum(x * x for x in range(1000000))Instead of:
total = sum([x * x for x in range(1000000)])Why: Generator expressions avoid creating full lists in memory, making them ideal for large datasets or streaming operations.
Use:
for i in range(1, 11):Instead of:
i = 1
while i <= 10:
print(i)
i += 1Why: range() is more concise and readable for loops with predictable bounds.
Use:
a, b, c = [1, 2, 3]Or:
for key, value in my_dict.items():
print(key, value)Instead of:
a = [1, 2, 3]
x = a[0]
y = a[1]
z = a[2]Why: Unpacking is cleaner and avoids repetitive indexing.
Use:
longest = max(words, key=len)Instead of:
longest = ''
for word in words:
if len(word) > len(longest):
longest = wordWhy: Built-in functions with key arguments simplify logic and improve performance.
Use:
my_dict.setdefault('key', []).append(value)Instead of:
if 'key' not in my_dict:
my_dict['key'] = []
my_dict['key'].append(value)Why: setdefault() reduces boilerplate when building grouped data structures.
Use:
s = slice(1, 5)
print(my_list[s])Instead of:
print(my_list[1:5])Why: slice objects are reusable and make slicing logic more explicit, especially in functions.
Use:
if isinstance(x, str):Instead of:
if type(x) == str:Why: isinstance() supports inheritance and is more flexible for type checking.
Use:
from collections import Counter
freq = Counter(my_list)Instead of:
freq = {}
for item in my_list:
freq[item] = freq.get(item, 0) + 1Why: Counter is optimized for counting and makes your intent clear.
Use:
for item in items:
if item == target:
print("Found!")
break
else:
print("Not found.")Why: The else clause runs only if the loop wasn’t broken — useful for search logic.
Use:
def main():
...
if __name__ == "__main__":
main()Why: This allows your script to be imported as a module without executing its main logic.
Use:
sorted_people = sorted(people, key=lambda p: p['age'])Instead of:
people.sort(key=lambda p: p['age'])Why: sorted() returns a new list and doesn’t mutate the original, which is safer and more flexible.
Use:
squared = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))Why: These functions express intent clearly and avoid manual loops.
Use:
for i, item in enumerate(items, start=1):
print(f"{i}. {item}")Why: You can start indexing from any number, which is great for user-facing lists or custom counters.
Use:
rounded = round(3.14159, 2) # 3.14Why: round() simplifies formatting and avoids manual string slicing or math tricks.
Use:
words = sentence.split()
sentence = ' '.join(words)Why: These are fast and expressive for breaking and rebuilding strings.
Use:
before, sep, after = line.partition(':')Why: Unlike split(), partition() always returns three parts and avoids index errors.
Use:
cleaned = text.replace('\n', ' ')Why: It’s fast and avoids regex overhead for basic replacements.
Use:
if a.casefold() == b.casefold():Why: casefold() is stronger than lower() for Unicode-aware comparisons.
Use:
if filename.endswith('.txt'):Why: These are faster and clearer than slicing or regex.
Use:
if 'error' in message:Why: It’s concise and avoids verbose find() or index() calls.
Use:
is_valid = bool(user_input)Why: Ensures consistent boolean logic when dealing with mixed types.
Use:
if abs(a - b) < tolerance:Why: It’s essential for comparing floating-point values or measuring distance.
Use:
if all(x > 0 for x in values):Why: It’s clean and avoids manual loop logic.
Use:
for item in reversed(my_list):Why: It’s more readable and avoids slicing or manual indexing.
Use:
sorted_items = sorted(items, reverse=True)Why: It’s safer than mutating the original list with sort() and reverse().
Use:
first = next((x for x in items if x > 0), None)Why: It’s efficient for finding the first match without crashing if none is found.
Use:
total = sum(x for x in values if x > 0)Why: It’s concise and avoids temporary lists.
Use:
lowest = min(values, default=0)Why: Prevents ValueError when the iterable is empty.
Use:
for key, value in my_dict.items():Why: It’s cleaner than looping over keys and accessing values manually.
Use:
value = my_dict.get('key', 'default')Why: Avoids KeyError and simplifies conditional logic.
Use:
value = my_dict.pop('key', None)Why: It’s efficient for consuming items while handling missing keys gracefully.
Use:
config.update(overrides)Why: It’s faster and cleaner than looping through keys manually.
Use:
def log(message, *args, **kwargs):
print(message.format(*args, **kwargs))Why: These allow your functions to accept variable numbers of positional and keyword arguments, making them more reusable and extensible.
Use:
def add(a, b, c): ...
nums = [1, 2, 3]
add(*nums)Why: Unpacking with * and ** simplifies passing arguments from existing data structures into functions.
Use:
for _ in range(3):
do_something()Why: Using _ signals that the variable is intentionally unused — it’s a convention that improves readability.
Use:
for item in items:
if item == target:
break
else:
print("Target not found")Why: The else clause runs only if the loop completes without a break, which is great for search patterns.
Use:
class User:
def __repr__(self):
return f"User(name={self.name!r})"Why: __repr__ is for developers (debugging), __str__ is for users (pretty printing). Implementing both improves clarity and logging.
Use:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = yWhy: __slots__ prevents the creation of __dict__, reducing memory usage for large numbers of instances.
Use:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14 * self._radius ** 2Why: @property lets you access methods like attributes, improving encapsulation and API clarity.
Use:
class Deck:
def __contains__(self, card):
return card in self.cardsWhy: This allows your custom objects to support in checks, making them behave like built-in containers.
Use:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1Why: This enables your objects to be used in for loops and comprehensions, making them more Pythonic.
Use:
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
print(f"Elapsed: {time.time() - self.start:.2f}s")Why: Custom context managers let you manage setup/teardown logic cleanly with with blocks.
Use:
class Math:
@staticmethod
def square(x): return x * x
@classmethod
def identity(cls): return cls()Why: @staticmethod is for utility methods that don’t access class or instance data. @classmethod is for methods that need the class, not an instance.
Use:
class Greeter:
def __call__(self, name):
return f"Hello, {name}!"Why: This allows instances to be invoked like functions, which is great for callbacks or configuration objects.
Use:
f"{value:.2f}" # or
"Value: {:.2f}".format(value)Why: These give you fine control over number formatting, padding, alignment, and more.
Use:
bin(10) # '0b1010'
hex(255) # '0xff'Why: These built-ins make it easy to work with binary, hexadecimal, and octal representations.
Use:
print(id(obj))Why: Useful for debugging reference vs. value issues, especially with mutable types.
Use:
print(globals().keys())Why: These functions let you inspect or manipulate the current namespace — powerful for debugging or metaprogramming.