Trey Hunner

Trey Hunner

@trey.io

Python & Django team trainer I help folks sharpen their Python skills with https://PythonMorsels.com 🐍πŸͺ I also blog at https://treyhunner.com & run https://Whereabouts.Earth PSF Fellow. YIMBY. 95% vegan.

4K followers219 following2.4K posts

Python Tip #299 (of 365): Return Boolean expressions directly. 🧡 Do this: def is_even(n): return n % 2 == 0 Not this: def is_even(n): if n % 2 == 0: return True else: return False #Python #DailyPythonTip

View full thread

But a condition that relies on truthiness might not be True or False: def is_number(value): if re.fullmatch(r"-?\d+(\.\d+)?", value): return True else: return False Returning that re.fullmatch call directly would return a re.Match object or None.

So use bool to get True or False: def is_number(value): return bool(re.fullmatch(r"-?\d+(\.\d+)?", value)) Whether bool(...) is clearer than an "if"/"else" is a bit more subjective, but I usually find it clearer. When I see bool(something) I think "we're performing a truthiness check".

pym.devTruthinessIn Python, truthiness is asking the question, what happens if I convert an object to a boolean. Every Python object is truthy by default. Truthiness in Python is about non-emptiness and non-zeroness.

Python Tip #299 (of 365): Return Boolean expressions directly. 🧡 Do this: def is_even(n): return n % 2 == 0 Not this: def is_even(n): if n % 2 == 0: return True else: return False #Python #DailyPythonTip

That "n % 2 == 0" condition will have a value of either True or False... which is also what we then return. So there's no need for an "if" statement and returning the result of the boolean expression directly is clearer.

Python Tip #298 (of 365): When creating lots of class instances, save memory with __slots__. 🧡 class Point: __slots__ = ("x", "y", "z") def __init__(self, x, y, z): (self.x, self.y, self.z) = (x, y, z) #Python #DailyPythonTip

View full thread

With __slots__, Python allocates fixed spots for only the named attributes: no per-instance dictionary at all. The trade-offs: 1. You can't add attributes that aren't in __slots__ 2. Anything that inspects __dict__ will no longer work with your instances (like the built-in vars() function)

Python Tip #298 (of 365): When creating lots of class instances, save memory with __slots__. 🧡 class Point: __slots__ = ("x", "y", "z") def __init__(self, x, y, z): (self.x, self.y, self.z) = (x, y, z) #Python #DailyPythonTip

Without that __slots__ line, every Point instance would store its attributes in a dictionary: >>> p = Point(1, 2, 3) >>> p.__dict__ {'x': 1, 'y': 2, 'z': 3} Dictionaries are flexible, but a dictionary for each instance takes a bit more memory than something smaller like a list/tuple would.

Python Tip #297 (of 365): Avoid making metaclasses if you can. 🧡 There's usually an easier tool for the job. Metaclasses customize the behavior of CLASSES the same way classes customize the behavior of their instances. #Python #DailyPythonTip

Metaclasses are powerful, but they can also make for code that's overly complex and hard to understand. Fortunately, many problems that COULD be solved with a metaclass often have a simpler solution.

You probably DON'T need a metaclass to: β€’ Customize instance creation: __new__ β€’ React to subclassing: __init_subclass__ β€’ Make a class subscriptable: __class_getitem__ β€’ Modify a class once it's made: a class decorator To customize your classes in some OTHER way, you may need a metaclass.

Python Tip #296 (of 365): Always use functools.wraps when making a decorator. 🧡 A decorator REPLACES a decorated function with the function that was returned from calling the decorator. #Python #DailyPythonTip

View full thread

functools.wraps borrows the name, signature, and docstring of the decorated function and updates the wrapper to match: >>> help(is_prime) Help on function is_prime in module __main__: is_prime(number) Return True if number is prime.

Python Tip #296 (of 365): Always use functools.wraps when making a decorator. 🧡 A decorator REPLACES a decorated function with the function that was returned from calling the decorator. #Python #DailyPythonTip

from functools import wraps def time_runs(func): @wraps(func) def wrapper(*args, **kwargs): start = perf_counter() return_value = func(*args, **kwargs) print(f"{func.__name__} ran in {perf_counter()-start} seconds") return return_value return wrapper

Without "@wraps(func)", help would show a function called wrapper that accepts any arguments and has no docstring: >>> @time_runs ... def is_prime(number): ... """Return True if number is prime.""" ... >>> help(is_prime) Help on function wrapper in module __main__: wrapper(*args, **kwargs)

Python Tip #295 (of 365): Avoid needless deep copying. 🧡 Often a deepcopy call can be replaced by a comprehension, a slice, or some other operation that makes new objects. When you copy a list of dictionaries, you get a NEW list pointing to the SAME dictionaries. #Python #DailyPythonTip

View full thread

Within the standard library, deepcopy is used in about 8 files. list(...) is used in over 100. If you don't mutate objects that don't belong to you, you'll rarely need deepcopy. I'm not saying "never use deepcopy" (it certainly CAN be useful), just that every deepcopy call should be questioned.

pym.devVariables and objects in PythonUnlike many programming languages, variables in Python are not buckets which "contain" objects. In Python, variables are pointers that "point" to objects.

Python Tip #295 (of 365): Avoid needless deep copying. 🧡 Often a deepcopy call can be replaced by a comprehension, a slice, or some other operation that makes new objects. When you copy a list of dictionaries, you get a NEW list pointing to the SAME dictionaries. #Python #DailyPythonTip

So if you plan to mutate those dictionaries, a shallow copy isn't enough. You could make a deep copy instead: from copy import deepcopy processed_data = deepcopy(tweet_data) for tweet in processed_data: tweet["date"] = datetime.strptime(tweet["date"], "%b %d %Y")

Or we could avoid that deepcopy call by making a new list of new dictionaries instead: processed_data = [ {**tweet, "date": datetime.strptime(tweet["date"], "%b %d %Y")} for tweet in tweet_data ] In Python, we usually prefer making NEW objects over mutating existing ones.

Python Tip #294 (of 365): Use exceptions to pass error messages to users. 🧡 Catch exceptions with messages users shouldn't see, and raise your own with a message they SHOULD see: class WeatherError(Exception): """Raised when we can't show the user a forecast.""" #Python #DailyPythonTip

View full thread

Why not print and exit right where the problem was detected? Because exiting within a random function is messy. Imagine if load_forecast was called more than once, sometimes with data that didn't come from a user. Raising an exception lets EACH caller decide how to handle the exceptional case.

Note that at the edge of your program, you'll want to catch JUST your custom exception type. A bare "except" (or "except Exception") there would also turn bugs in your code into one-line messages for the end user, hiding any traceback you'd want to see when troubleshooting them.

Python Tip #294 (of 365): Use exceptions to pass error messages to users. 🧡 Catch exceptions with messages users shouldn't see, and raise your own with a message they SHOULD see: class WeatherError(Exception): """Raised when we can't show the user a forecast.""" #Python #DailyPythonTip

View full thread

Then, at the very edge of your program, catch JUST that exception type and show the user its message: def main(): try: forecast = load_forecast(sys.argv[1]) except WeatherError as error: sys.exit(f"Error: {error}") print(forecast["high"])

Python Tip #294 (of 365): Use exceptions to pass error messages to users. 🧡 Catch exceptions with messages users shouldn't see, and raise your own with a message they SHOULD see: class WeatherError(Exception): """Raised when we can't show the user a forecast.""" #Python #DailyPythonTip

def load_forecast(path): try: with open(path) as file: return json.load(file) except FileNotFoundError as error: raise WeatherError(f"No forecast file found at {path}") from error That "from error" keeps the original exception attached (see tip #143).

bsky.appTrey Hunner (@trey.io)Python Tip #143 (of 365): Re-raise exceptions deliberately 🧡 When you need to re-raise an exception, you have 2 options: 1. Use a bare "raise" statement 2. Use "raise" with a "from" clause #Python…

Python Tip #293 (of 365): Write context managers with contextlib.contextmanager. 🧡 Instead of making a class with __enter__ and __exit__ methods, we can make a context manager out of a function: #Python #DailyPythonTip

View full thread

The "try"/"finally" makes sure the environment variable is restored even if an exception is raised in the "with" block. The generator function you decorate must only "yield" one time: that "yield" shouldn't be in a loop and there shouldn't be more than one of them.

To customize what your context manager returns to its "with" block, pass a value to the "yield" statement. The contextmanager decorator is great for SIMPLE context managers. If your __enter__ or __exit__ methods need a lot of logic, using a class might be easier to understand.

Python Tip #293 (of 365): Write context managers with contextlib.contextmanager. 🧡 Instead of making a class with __enter__ and __exit__ methods, we can make a context manager out of a function: #Python #DailyPythonTip

@contextmanager def set_env_var(name, new_value): original_value = os.environ.get(name) os.environ[name] = new_value try: yield finally: if original_value is None: del os.environ[name] else: os.environ[name] = original_value

We can use it in a "with" block, like any other context manager: >>> with set_env_var('USER', 'akin'): ... print(os.environ['USER']) ... akin The code BEFORE the "yield" runs when the "with" block is entered and the code AFTER runs when the block exits.