Using guard in Python
Bringing Swift’s Guard Statement to Python
One of the most convenient constructs in Swift is the guard
statement. It provides a clear and readable way to check preconditions before executing code. If a condition isn’t met, the guard
statement allows early exit, improving readability and reducing nested code blocks. Unfortunately, Python does not have a native guard
statement. However, I decided to create a decorator-based implementation that offers similar functionality.
Implementing a Guard Decorator in Python
Since Python does not support inline guard statements like Swift, the next best approach is using decorators. This method allows us to declare preconditions above a function definition, making it clear which conditions must be satisfied before execution.
from functools import wraps
from typing import Any, Callable
def guard(condition_func, exception_func=lambda: ValueError("Guard condition failed"), raiseExceptionOnException=False):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if raiseExceptionOnException:
try:
if not condition_func(*args, **kwargs):
raise exception_func()
except Exception:
raise exception_func()
else:
if not condition_func(*args, **kwargs):
raise exception_func()
return func(*args, **kwargs)
return wrapper
return decorator
def guard_input(condition_func: Callable[..., bool], message: str):
def decorator(func: Callable[..., Any]):
return guard(condition_func, lambda: ValueError(message))(func)
return decorator
How It Works
The guard
decorator takes:
condition_func
: A function that determines whether execution should proceed.exception_func
: A function that returns an exception to be raised if the condition fails (defaulting toValueError
).raiseExceptionOnException
: IfTrue
, exceptions raised during condition evaluation trigger the exception function.
This approach makes it explicit which conditions must be met before executing a function.
Demonstrating the Guard Decorator with Tests
The following tests not only validate the guard
decorator but also serve as a practical demonstration of how it works in different scenarios using pytest
:
import pytest
from system.guard import guard
class CustomGuardTestException(Exception):
pass
@guard(lambda value: value is not None, lambda: CustomGuardTestException("Value is None"))
@guard(lambda value: value != "TEST", lambda: CustomGuardTestException("Value is Test, this is not allowed"))
def example_function(value):
print(f"Value is {value}")
# Demonstrating valid and invalid function calls
def test_guard_succeeds():
"""Demonstrates that the function executes normally when conditions are met."""
example_function("TEST2")
example_function("TEST4")
def test_guard_fails():
"""Shows how the function raises an exception when conditions are not met."""
with pytest.raises(CustomGuardTestException):
example_function("TEST")
with pytest.raises(CustomGuardTestException):
example_function(None)
These tests illustrate that the guard
decorator prevents execution if the conditions are not satisfied, making precondition enforcement explicit.
Example Use Case
Below is a more practical example where the guard
decorator ensures that a function receives a valid data connection:
@guard( # Ensure a data connection is provided
lambda _, data_connection: data_connection is not None,
lambda: QueryEngineError("No data connection provided"),
)
@guard( # Prevent the query engine from running on data connections that are not allowed
lambda _, data_connection: data_connection["allowQueryEngine"] is True,
lambda: QueryEngineError(
"This data connection is not configured to be used by the Query Engine"
),
raiseExceptionOnException=True,
)
def run_query(query: str, data_connection: dict) -> tuple:
if query == "TEST":
return [{"test": "test"}], ["test"]
db_type_to_function = {
"sqlite3": run_sqlite_query,
"MySQL": run_mysql_query,
"PostgreSQL": run_postgres_query,
"MSSQL": run_mssql_query,
}
db_type = data_connection["resolvedParameters"]["type"]
engine_query_function = db_type_to_function.get(db_type)
if not engine_query_function:
raise ValueError(f"Unsupported database type: {db_type}")
return engine_query_function(query, data_connection)
Conclusion
The guard
decorator brings a Swift-like precondition mechanism to Python. While not identical to Swift’s guard
statement, it improves readability and maintainability by making preconditions explicit. This approach can be particularly useful when validating function inputs and ensuring constraints are met before execution.