Skip to content

Latest commit

 

History

History
1078 lines (686 loc) · 19.3 KB

File metadata and controls

1078 lines (686 loc) · 19.3 KB

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.


🧼 1. Prefer is None over == None

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.


🧪 2. Use truthy and falsy values wisely

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().


🧵 3. Use list comprehensions

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.


🧰 4. Use enumerate() instead of manual indexing

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.


🧠 5. Use with for resource management

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.


🧹 6. Avoid mutable default arguments

Use:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Instead of:

def add_item(item, items=[]):
    items.append(item)
    return items

Why: Mutable default arguments persist across function calls, which can lead to unexpected behavior. Using None avoids shared state bugs.


🧭 7. Use try/except for EAFP (Easier to Ask Forgiveness than Permission)

Use:

try:
    value = my_dict['key']
except KeyError:
    value = default

Instead of:

if 'key' in my_dict:
    value = my_dict['key']
else:
    value = default

Why: EAFP is more idiomatic in Python and often more performant, especially when the check is expensive or the failure is rare.


🧾 8. Format strings with f-strings

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+.


🧱 9. Escape braces in f-strings with double braces

Use:

print(f"{{username}} is not the same as {username}")

Instead of:

print(f"{username} is not the same as {}")  # ❌ SyntaxError

Why: In f-strings, {} is used for expressions. To include literal braces, you must double them ({{ and }}).


🧮 10. Use zip() to iterate over multiple iterables

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.


🔍 11. Use any() and all() for concise checks

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.


🧼 12. Avoid comparing boolean expressions to True or False

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.


🧮 13. Use divmod() for quotient and remainder

Use:

q, r = divmod(10, 3)

Instead of:

q = 10 // 3
r = 10 % 3

Why: divmod() is more efficient and expressive when you need both the quotient and remainder.


🧠 14. Use set for membership tests

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.


🧮 15. Use tuple unpacking for swapping

Use:

a, b = b, a

Instead of:

temp = a
a = b
b = temp

Why: Tuple unpacking is more concise and avoids temporary variables. It’s a Pythonic way to swap values.


🧠 16. Use get() for safe dictionary access

Use:

value = my_dict.get('key', default)

Instead of:

if 'key' in my_dict:
    value = my_dict['key']
else:
    value = default

Why: get() simplifies access and avoids verbose conditional logic. It’s ideal for optional keys.


🧼 17. Use join() to concatenate strings

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.


🧠 18. Use reversed() and sorted() instead of manual sorting

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.


🧠 19. Use generator expressions for memory efficiency

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.


🧮 20. Use range() smartly

Use:

for i in range(1, 11):

Instead of:

i = 1
while i <= 10:
    print(i)
    i += 1

Why: range() is more concise and readable for loops with predictable bounds.


🧠 21. Use unpacking in loops and assignments

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.


🧮 22. Use max() and min() with key for custom comparisons

Use:

longest = max(words, key=len)

Instead of:

longest = ''
for word in words:
    if len(word) > len(longest):
        longest = word

Why: Built-in functions with key arguments simplify logic and improve performance.


🧠 23. Use dict.setdefault() to simplify conditional insertion

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.


🧮 24. Use slice objects for reusable slicing

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.


🧠 25. Use type() and isinstance() appropriately

Use:

if isinstance(x, str):

Instead of:

if type(x) == str:

Why: isinstance() supports inheritance and is more flexible for type checking.


🧮 26. Use collections.Counter for quick frequency counts

Use:

from collections import Counter
freq = Counter(my_list)

Instead of:

freq = {}
for item in my_list:
    freq[item] = freq.get(item, 0) + 1

Why: Counter is optimized for counting and makes your intent clear.


🧠 27. Use for/else for search patterns

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.


🧮 28. Use __name__ == "__main__" to make scripts reusable

Use:

def main():
    ...

if __name__ == "__main__":
    main()

Why: This allows your script to be imported as a module without executing its main logic.


🧠 29. Use sorted() with custom keys

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.


🧮 30. Use map() and filter() for functional-style transformations

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.


🧠 31. Use enumerate(start=n) to customize index

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.


🧮 32. Use round() for clean numeric output

Use:

rounded = round(3.14159, 2)  # 3.14

Why: round() simplifies formatting and avoids manual string slicing or math tricks.


🧠 33. Use str.split() and str.join() for text parsing

Use:

words = sentence.split()
sentence = ' '.join(words)

Why: These are fast and expressive for breaking and rebuilding strings.


🧮 34. Use str.partition() for safe splitting

Use:

before, sep, after = line.partition(':')

Why: Unlike split(), partition() always returns three parts and avoids index errors.


🧠 35. Use str.replace() for simple substitutions

Use:

cleaned = text.replace('\n', ' ')

Why: It’s fast and avoids regex overhead for basic replacements.


🧮 36. Use str.casefold() for aggressive case-insensitive comparison

Use:

if a.casefold() == b.casefold():

Why: casefold() is stronger than lower() for Unicode-aware comparisons.


🧠 37. Use str.startswith() and str.endswith() for prefix/suffix checks

Use:

if filename.endswith('.txt'):

Why: These are faster and clearer than slicing or regex.


🧮 38. Use in for substring checks

Use:

if 'error' in message:

Why: It’s concise and avoids verbose find() or index() calls.


🧠 39. Use bool() to normalize truthy values

Use:

is_valid = bool(user_input)

Why: Ensures consistent boolean logic when dealing with mixed types.


🧮 40. Use abs() for magnitude comparisons

Use:

if abs(a - b) < tolerance:

Why: It’s essential for comparing floating-point values or measuring distance.


🧠 41. Use all() with generator expressions for universal conditions

Use:

if all(x > 0 for x in values):

Why: It’s clean and avoids manual loop logic.


🧮 42. Use reversed() for reverse iteration

Use:

for item in reversed(my_list):

Why: It’s more readable and avoids slicing or manual indexing.


🧠 43. Use sorted() with reverse=True for descending order

Use:

sorted_items = sorted(items, reverse=True)

Why: It’s safer than mutating the original list with sort() and reverse().


🧮 44. Use next() with default to avoid StopIteration

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.


🧠 45. Use sum() with generator expressions

Use:

total = sum(x for x in values if x > 0)

Why: It’s concise and avoids temporary lists.


🧮 46. Use min() and max() with default to avoid errors

Use:

lowest = min(values, default=0)

Why: Prevents ValueError when the iterable is empty.


🧠 47. Use dict.items() for key-value iteration

Use:

for key, value in my_dict.items():

Why: It’s cleaner than looping over keys and accessing values manually.


🧮 48. Use dict.get() with fallback

Use:

value = my_dict.get('key', 'default')

Why: Avoids KeyError and simplifies conditional logic.


🧠 49. Use dict.pop() to remove and return a value

Use:

value = my_dict.pop('key', None)

Why: It’s efficient for consuming items while handling missing keys gracefully.


🧮 50. Use dict.update() to merge dictionaries

Use:

config.update(overrides)

Why: It’s faster and cleaner than looping through keys manually.


🧠 51. Use *args and **kwargs for flexible function signatures

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.


🧮 52. Use * to unpack iterables

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.


🧠 53. Use _ for throwaway variables

Use:

for _ in range(3):
    do_something()

Why: Using _ signals that the variable is intentionally unused — it’s a convention that improves readability.


🧮 54. Use else with while and for for post-loop logic

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.


🧠 55. Use object.__repr__ and __str__ for custom class display

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.


🧮 56. Use __slots__ to save memory in classes

Use:

class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

Why: __slots__ prevents the creation of __dict__, reducing memory usage for large numbers of instances.


🧠 57. Use property for computed attributes

Use:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14 * self._radius ** 2

Why: @property lets you access methods like attributes, improving encapsulation and API clarity.


🧮 58. Use __contains__ to customize in behavior

Use:

class Deck:
    def __contains__(self, card):
        return card in self.cards

Why: This allows your custom objects to support in checks, making them behave like built-in containers.


🧠 59. Use __iter__ and __next__ to make objects iterable

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 + 1

Why: This enables your objects to be used in for loops and comprehensions, making them more Pythonic.


🧮 60. Use __enter__ and __exit__ to create custom context managers

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.


🧠 61. Use @staticmethod and @classmethod appropriately

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.


🧮 62. Use __call__ to make objects behave like functions

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.


🧠 63. Use format() or f-strings for dynamic formatting

Use:

f"{value:.2f}"  # or
"Value: {:.2f}".format(value)

Why: These give you fine control over number formatting, padding, alignment, and more.


🧮 64. Use bin(), hex(), and oct() for base conversions

Use:

bin(10)   # '0b1010'
hex(255)  # '0xff'

Why: These built-ins make it easy to work with binary, hexadecimal, and octal representations.


🧠 65. Use id() to inspect object identity

Use:

print(id(obj))

Why: Useful for debugging reference vs. value issues, especially with mutable types.


🧮 66. Use globals() and locals() for dynamic introspection

Use:

print(globals().keys())

Why: These functions let you inspect or manipulate the current namespace — powerful for debugging or metaprogramming.