#dailypythontip — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #dailypythontip, aggregated by home.social.
-
Python Tip #231 (of 365):
Use class methods as alternate constructors.
>>> s = Square.from_area(100)
>>> s.length
10.0The classmethod decorator makes a method that's meant to be called on the class itself (not on an instance):
import math
class Square:
def __init__(self, length, color=None):
self.length = length
self.color = color
@classmethod
def from_area(cls, area, color=None):
return cls(math.sqrt(area), color)🧵 (1/4)
-
Python Tip #230 (of 365):
Use a single underscore prefix for internal attributes.
This _digits attribute is private... but only by convention:
import re
class PhoneNumber:
def __init__(self, number):
self._digits = re.sub(r"\D", "", number)
...This (not fully implemented) class includes str() and an area_code attribute as its public interface:
>>> wh = PhoneNumber("(202) 456-1414")
>>> print(wh)
202-456-1414
>>> wh.area_code
'202'🧵 (1/3)
-
Python Tip #229 (of 365):
Don't overuse classes.
Writing Python code does NOT require writing classes.
Unlike many programming languages, you can accomplish quite a bit in Python without ever making a class.
The most common reason to write a class is that a framework REQUIRES one (like Django's Model classes).
The best reason to write your OWN class: you have data and functionality that clearly belong together.
🧵 (1/3)
-
Python Tip #228 (of 365):
Don't catch NameError exceptions. 🧵
This is an issue:
try:
start, end = parse_date(row["start"]), parse_date(row["end"])
except (ValueError, TypeError, KeyError, NameError):
print(f"Invalid date on line {n}", file=sys.stderr)
continueA NameError is raised when a variable isn't defined... which usually means a variable name was MISSPELLED.
🧵 (1/3)
-
Python Tip #227 (of 365):
Don't rely on clever implementation details. 🧵
In CPython, two 3's are identical:
>>> x = 3
>>> y = 3
>>> x is y
TrueBut two 300's are not always identical:
>>> x = 300
>>> y = 300
>>> x is y
FalseDon't rely on this. Compare numbers with ==.
In CPython, integers from -5 to 256 are created on launch, so every 200 is identical to every other 200.
Bigger numbers are only identical sometimes:
>>> x, y = 300, 300
>>> x is y
True🧵 (1/2)
-
Python Tip #226 (of 365):
Don't assign to global variables from within a function.
You CAN do this in Python. But you really shouldn't.
Yesterday's tip noted that assignments within a function NORMALLY make local variables.
But they don't always...
Python's "global" statement is the exception:
>>> phrase = "Hi world"
>>> def set_phrase(name):
... global phrase
... phrase = f"Hello {name}"
...
>>> set_phrase("Trey")
>>> phrase
'Hello Trey'🧵 (1/2)
-
Python Tip #225 (of 365):
Avoid creating local variables that share a name with a global variable.
This is called SHADOWING, and it can make for confusing code.
Assigning to a variable inside a function normally makes a LOCAL variable:
>>> phrase = "Hi!"
>>> def set_phrase(name):
... phrase = f"Hello {name}"
>>> set_phrase("Trey")
>>> phrase
'Hi!'The global phrase never changed. The function makes a LOCAL phrase that disappears when it returns.
🧵 (1/3)
-
Python Tip #224 (of 365):
Constants that are specific to a class often belong ON that class.
class Circle:
max_radius = 100
def __init__(self, radius):
if radius > self.max_radius:
raise ValueError(f"Radius can't exceed {self.max_radius}")
self.radius = radiusBut, why make max_radius a class attribute instead of a module-level MAX_RADIUS constant...?
🧵 (1/3)
-
Python Tip #223 (of 365):
Consider copying mutable arguments in your class initializers.
What happens if two objects are given the SAME list?
>>> initial_tasks = ['Watch screencast', 'Brush teeth']
>>> mon = TodoList(initial_tasks)
>>> tue = TodoList(initial_tasks)It depends...
🧵 (1/4)
-
Python Tip #222 (of 365):
Be careful with mutable default values in function definitions.
This class has a bug:
class TodoList:
def __init__(self, tasks=[]):
self.tasks = tasksdef add_task(self, task):
self.tasks.append(task)Every TodoList object made without arguments shares the SAME list:
>>> mon = TodoList()
>>> tue = TodoList()
>>> mon.add_task("Work on Python exercise")
>>> tue.tasks
['Work on Python exercise']🧵 (1/3)
-
Python Tip #221 (of 365):
Implement appropriate dunder methods.
Dunder methods are a contract between your class and the Python interpreter.
• __add__ & __radd__ power +
• __eq__ powers ==
• __len__ powers len()Dunder methods are what third-party library authors use to make their objects feel like NATIVE Python objects.
Django QuerySet objects support indexing, iteration, len(...), and truthiness.
And Pandas DataFrames support a whole BUNCH of syntax.
🧵 (1/2)
-
Python Tip #220 (of 365):
Trying quack like a duck? Seek help.
When implementing a common Python protocol (a sequence, a mapping, etc.), inherit from the matching abstract base class (ABC):
from collections.abc import MutableSequence
class MyCustomList(MutableSequence):
...Benefits:
1. isinstance & issubclass checks work well (i.e. isinstance(x, Sequence))
2. Python will complain LOUDLY if you forget to implement a required method for the protocol
🧵 (1/3)
-
Python Tip #219 (of 365):
Validate your ducks with goose typing.
You can use isinstance() WHILE duck typing. This is called goose typing (coined by Alex Martelli).
>>> from collections.abc import Iterable
>>> isinstance([1, 2, 3], Iterable)
TruePoint doesn't inherit from Iterable:
class Point:
def __init__(self, x, y): self.x, self.y = x, y
def __iter__(self): yield from (self.x, self.y)And yet...
>>> isinstance(Point(1, 2), Iterable)
True🧵 (1/3)
-
Python Tip #218 (of 365):
Don't needlessly convert between data types.
Looping over a file once? There's no reason to convert it to a list first:
for line in my_file:
print(line, end="")Another example: if you're looping over a set, there's no reason to convert it to a list either:
>>> colors = {'red', 'blue', 'yellow'}
>>> for color in colors:
... print(color)
...
red
yellow
blue"for" loops don't need a list. They accept ANY iterable.
🧵 (1/2)
-
Python Tip #217 (of 365):
Think in terms of the minimum viable duck.
In other words: "what's the least this object needs to do to work in this context?"
For example, csv.reader is usually given a file object... but it accepts ANY iterable of delimited lines:
>>> import csv
>>> rows = ['a,b,c', '1,2,3']
>>> list(csv.reader(rows))
[['a', 'b', 'c'], ['1', '2', '3']]🧵 (1/4)
-
Python Tip #216 (of 365):
In Python, we often care more about the behavior of an object than its type.
We say "if it looks like a duck and quacks like a duck, it's a duck".
When we check for duckness, we don't test DNA, we observe behavior.
Instead of asking "what TYPE can I use here?", ask "does my object have the BEHAVIOR that's expected here?"
🧵 (1/3)
-
Python Tip #215 (of 365):
Know what it means to be a "sequence" in Python.
Lists, strings, and tuples are all sequences:
>>> fruits = ["apple", "lemon", "pear"]
>>> coordinates = (1, 8, 2)
>>> greeting = "Hi y'all!"A sequence is an ordered collection that:
1. Has a length
2. Can be indexed from "0" up to one less than its length
3. Can be looped over>>> len(fruits)
3
>>> fruits[0]
'apple'
>>> for c in coordinates: print(c)
1
8
2🧵 (1/3)
-
Python Tip #214 (of 365):
Use truthiness for integers with caution.
I DO rely on truthiness checks for integers, but not always. I would recommend only using them when they seem to improve readability.
I find this:
if hours: ...More readable than this:
if hours > 0: ...But I find this:
if number % 2 != 0: ...More readable than this:
if number % 2: ...🧵 (1/2)
-
Python Tip #213 (of 365):
Replace "or"-chained equality comparisons with a containment check.
Instead of this:
is_vowel = c == "a" or c == "e" or c == "i" or c == "o" or c == "u"
You can write this:
is_vowel = c in ("a", "e", "i", "o", "u")
In fact, you could even give that tuple a name:
vowels = ("a", "e", "i", "o", "u")
is_vowel = c in vowels🧵 (1/2)
-
Python Tip #212 (of 365):
Use any() and all() to check a condition across every item in an iterable
This:
any_negative = False
for n in numbers:
if n < 0:
any_negative = True
breakCan be replaced with:
any_negative = any(n < 0 for n in numbers)
And this:
all_good = True
for item in iterable:
if not condition(item):
all_good = False
breakCan be replaced with:
all_good = all(condition(item) for item in iterable)
🧵 (1/2)
-
Python Tip #211 (of 365):
Consider how your Boolean expressions read in English
For example, you could write "10 > x" or you could write "x < 10".
They check the same thing, but I find the latter one more readable.
Steve McConnell's Code Complete recommends writing numeric tests in "number-line order": order the values left to right, from smallest to largest.
Follow that rule and you'll always use < and <=, never > and >=.
I don't follow that advice...
🧵 (1/3)
-
Python Tip #210 (of 365):
Instead of an "if" under an "else", consider using "elif".
You likely know about Python's "elif" statement, but you might occasionally underuse it.
If you find yourself writing an "if" statement just under an "else", ask "should I use an "elif" here instead?"
🧵 (1/3)
-
Python Tip #209 (of 365):
Remember Python's "inline ifs"
Python doesn't have traditional ternary operators (cond ? one : two) but instead has an inline "if" expression (technically a "conditional expression") for the same purpose.
Instead of:
if amount == 1:
noun = "item"
else:
noun = "items"We can write
noun = "item" if amount == 1 else "items"
The condition is in the MIDDLE. The first value is the "true" case and the last is the "false" case.
🧵 (1/2)
-
Python Tip #208 (of 365):
Break up long boolean expressions
Have a long condition in your "if" statement?
You could:
1. Wrap it in parentheses to break it up over multiple lines
2. Use variables to give names to each sub-expression
3. Use functions to name sub-expressions🧵 (1/4)
-
Python Tip #207 (of 365):
Get fancy with your subprocess.run calls.
When launching a subprocess, consider passing the keyword arguments capture_output, text, and check to subprocess.run.
Passing capture_output=True will capture both stdout & stderr from the subprocess.
Passing text=True will decode the captured output into strings (instead of providing raw bytes).
Passing check=True will raise an exception if the subprocess exited with an error code.
🧵 (1/2)
-
Python Tip #206 (of 365):
Know your combination and permutation tools in Python.
Python ships with tools for combinations, permutations, and random selections.
The itertools module includes various combinatorics-oriented utilities:
from itertools import combinations, combinations_with_replacement, permutations, product
This joins tuples of characters into a comma-separated string:
def join(groups):
return ", ".join("".join(g) for g in groups)🧵 (1/5)
-
Python Tip #205 (of 365):
Use statistics.multimode to get the most common items.
To get the most common items in an iterable, you might be tempted to use collections.Counter. Consider using statistics.multimode instead.
Compare this:
counts = collections.Counter(items)
[(_, top_count)] = counts.most_common(1)
commonest = [
item
for item, count in counts.items()
if count == top_count
]To this:
commonest = statistics.multimode(items)
Simpler, right?
-
Python Tip #204 (of 365):
Generate unique identifiers with the uuid module
>>> import uuid
>>> uid = uuid.uuid4()
>>> uid
UUID('922a6cea-c3e9-473d-bf88-efb10793c931')
>>> str(uid)
'922a6cea-c3e9-473d-bf88-efb10793c931'UUID stands for universally unique identifiers.
The uuid module has functions for UUID version 1, 3, 4, 5, 6, 7, and 8 from RFC 4122/9562.
Versions 7 and 8 were added in Python 3.14.
🧵 (1/2)
-
Python Tip #203 (of 365):
When you need precise decimal numbers, use decimal.Decimal
Floating point numbers have precision gotchas.
For example, 0.1 + 0.02 isn't 0.12:
>>> one_two = 0.1 + 0.02
>>> one_two == 0.12
False
>>> one_two
0.12000000000000001Using Decimal fixes this:
>>> from decimal import Decimal
>>> one_two = Decimal("0.1") + Decimal("0.02")
>>> one_two == Decimal("0.12")
True
>>> one_two
Decimal('0.12')Note that we pass strings to Decimal.
🧵 (1/4)
-
Python Tip #202 (of 365):
When prompting the user to enter a password, use getpass
The getpass.getpass function prompts the user to enter a password and what the user types is completely hidden:
>>> from getpass import getpass
>>> password = getpass()
Password:🧵 (1/2)