What is the right exception for an unmet precondition? - python

What is the appropriate exception to raise in a function to signal that a precondition was not met?
Examples:
def print_stats(name, age):
if name is None:
raise Exception("name cannot be None")
if not type(name) is str:
raise Exception("name must be a string")
if age is None:
raise Exception("age cannot be None")
if age < 0:
raise Exception("age cannot be negative")
print("{0} is {1} years old".format(name, age))

You should use both TypeError and ValueError.
The first three exceptions should be TypeErrors because we are signaling that the arguments are of an incorrect type. From the docs:
exception TypeError
Raised when an operation or function is applied to an object of
inappropriate type. The associated value is a string giving details
about the type mismatch.
The last exception however should be a ValueError because age is the correct type but has an incorrect value (it is negative). From the docs:
exception ValueError
Raised when a built-in operation or function receives an argument that
has the right type but an inappropriate value, and the situation is
not described by a more precise exception such as IndexError.

I also think you should use TypeError and ValueError but you can also improve the way you apply your preconditions.
Some time ago I was playing with postconditions and preconditions. Python allows you to write a much more elegant solution using decorators instead those if statements inside the function.
For instance:
def precondition(predicate, exception, msg): // 1
def wrapper(func):
def percond_mechanism(*args, **kwargs): // 2
if predicate(*args, **kwargs):
return func(*args, **kwargs) // 3
else:
raise exception(msg) // 4
return percond_mechanism
return wrapper
The condition, the exception you want to raise if the condition is not fulfilled and the message you want to show.
This part check if the condition is fulfilled.
If everething is ok, just return the result of the original funcion.
If not, raise the exception you pass with your message.
Now you can write your function like this:
#precondition(lambda name, age: name is not None, ValueError, "name can't be None")
#precondition(lambda name, age: type(name) is str, TypeError, "name has to be str")
# You can continue adding preconditions here.
def print_stats(name, age):
print("{0} is {1} years old".format(name, age))
This way is much more easier to read what can and can't be done. And actualy, you can use this precondition decorator in any function you want to.

I would go with ValueError:
Raised when a built-in operation or function receives an argument that
has the right type but an inappropriate value, and the situation is
not described by a more precise exception such as IndexError.
Source: https://docs.python.org/2/library/exceptions.html

I like Raydel Miranda's answer using decorator pre-conditions for the function. Here is an somewhat similar approach that, instead of decorators, uses introspection and eval. It would be less efficient, but arguably slightly more concise and expressive.
import inspect
class ValidationError(ValueError):
pass
def validate(assertion, exc=ValidationError, msg=''):
"""
Validate the given assertion using values
from the calling function or method. By default,
raises a `ValidationException`, but optionally
raises any other kind of exeception you like.
A message can be provided, and will be formatted
in the context of the calling function. If no
message is specified, the test assertion will be
recapitulated as the cause of the exception.
"""
frame = inspect.currentframe().f_back
f_locals, f_globals = frame.f_locals, frame.f_globals
result = eval(assertion, f_globals, f_locals)
if result:
return
else:
if msg:
msg = msg.format(**f_locals)
else:
msg = 'fails test {0!r}'.format(assertion)
raise(exc(msg))
def r(name):
validate('isinstance(name, str)', msg='name must be str (was {name!r})')
validate('name.strip() != ""', msg='name must be non-empty (was {name!r})')
print(name,)
def r2(name, age):
validate('isinstance(name, str)', TypeError, 'name must be str (was {name!r})')
validate('name.strip() != ""', ValueError, 'name must be non-empty (was {name!r})')
validate('isinstance(age, int)', TypeError, 'age must be int (was {age!r})')
validate('age >= 0', ValueError, 'age must be non-negative (was {age!r})')
print(name,)
r('Joe')
r('')
r2('Dale', -3)
r2('Dale', None)
This will raise exceptions such as:
ValidationError: name must be non-empty (was '')
Also nice: If you don't specify any message, it still gives reasonable output. For example:
def r2simple(name, age):
validate('isinstance(name, str)')
validate('name.strip() != ""')
validate('isinstance(age, int)')
validate('age >= 0')
print(name,)
r2simple('Biff', -1)
Yields:
ValidationError: fails test 'age >= 0'
This will work under either Python 2 or 3.

Related

Python OOP only initialise object if function arguments are met

How would I only allow an object to be created if the arguments are met? I tried using error handling but the object still gets created regardless of the value inputted.
class Age(object):
def __init__(self, age):
try:
if age > 5:
self.testAge = age
else:
raise ValueError('Number is too low')
except ValueError as exp:
print(f"Error: {exp}")
def __str__(self):
return f"Testing"
test = Age(3)
print(test)
In the example, I only want an object to be created if the Age is greater than 5. I use 3 as a test, the error is handled but the object is still created.
When you raise the exception, you except it again
You can either remove the try/except or re-raise the Exception
try:
function_which_may_raise()
except Exception as ex:
print("something went wrong")
raise ex # re-raise Exception

Passing information up the stack in an Exception?

I am getting an exception deep in some loop in a function in a long running process. If I get an exception, I would like to log a thing that is at the index in the loop at which the exception occurred. Unfortunately, the information that I need isn't available in the current function... it's in the next function up the stack. However, the index isn't available in the next function up the stack, it is only available in the current function. In order to log the appropriate info, I therefore need information from two function calls at different nesting levels. How do I pass information between functions in an Exception?
For example:
def foo():
information_I_need = ["some", "arbitrary", "things"]
data_operated_on = list(range(0, 10*len(information_I_need), 10)) #0,10,20
#NB: these two lists are the same size
try:
bar(data_operated_on)
except ValueError as e:
i = e.get_the_index_where_bar_failed()
print(information_I_need[i])
def bar(aoi):
for i in range(len(aoi)):
try:
fails_on_10(aoi[i])
except ValueError as e:
e.add_the_index_where_bar_failed(i)
raise e
def fails_on_10(n):
if n == 10:
raise ValueError("10 is the worst!")
The expected behavior here would be that a call to foo() prints "arbitrary".
In this example, bar has information (namely, the index i) that foo needs to correctly report the problem. How do I get that information from bar up to foo?
You can add the index as an attribute of the exception object.
It's best to do this with a custom exception class, rather than using one of the built-in exceptions.
class BadInformation(Exception):
def __init__(self, message, index):
# py2/3 compat
# if only targeting py3 you can just use super().__init__(message)
super(BadInformation, self).__init__(message)
self.bad_index = index
def foo():
information_I_need = ["some", "arbitrary", "things"]
data_operated_on = list(range(0, 10*len(information_I_need), 10)) #0,10,20
#NB: these two lists are the same size
try:
bar(data_operated_on)
except BadInformation as e:
i = e.bad_index
print(information_I_need[i])
def bar(aoi):
# if you need both the index and value, use `enumerate()`
for index, value in enumerate(aoi):
try:
fails_on_10(value)
except ValueError as e:
raise BadInformation(str(e), index)
## on py 3 you may want this instead
## to keep the full traceback
# raise BadInformation(str(e), index) from e
def fails_on_10(n):
if n == 10:
raise ValueError("10 is the worst!")

How to limit the permitted values that can be passed to a method parameter (Using Type Hinting to allow Static Code Analysis)

In Python 3, I want to limit the permitted values that are passed to this method:
my_request(protocol_type, url)
Using type hinting I can write:
my_request(protocol_type: str, url: str)
so the protocol and url are limited to strings, but how can I validate that protocol_type accepts only limited set of values, e.g. 'http' and 'https'?
One way is to write code in the method to validate that the value passed in is 'http' or 'https', something in the lines of:
if (protocol_type == 'http') or (protocol_type == 'https'):
Do Something
else:
Throw an exception
Which will work fine during runtime, but doesn't provide an indication of a problem while writing the code.
This is why I prefer using Enum and the type-hinting mechanism that Pycharm and mypy implement.
For the code example below you will get a warning in Pycharm from its code-inspection, see attached screenshot.
The screenshot shows that if you enter a value that is not enum you will get the "Expected Type:..." warning.
Code:
"""Test of ENUM"""
from enum import Enum
class ProtocolEnum(Enum):
"""
ENUM to hold the allowed values for protocol
"""
HTTP: str = 'http'
HTTPS: str = 'https'
def try_protocol_enum(protocol: ProtocolEnum) -> None:
"""
Test of ProtocolEnum
:rtype: None
:param protocol: a ProtocolEnum value allows for HTTP or HTTPS only
:return:
"""
print(type(protocol))
print(protocol.value)
print(protocol.name)
try_protocol_enum(ProtocolEnum.HTTP)
try_protocol_enum('https')
Output:
<enum 'ProtocolEnum'>
http
HTTP
I guess you can use decorators, I have a similar situation but I wanted to validate the parameter types:
def accepts(*types):
"""
Enforce parameter types for function
Modified from https://stackoverflow.com/questions/15299878/how-to-use-python-decorators-to-check-function-arguments
:param types: int, (int,float), if False, None or [] will be skipped
"""
def check_accepts(f):
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
if t:
assert isinstance(a, t), \
"arg %r does not match %s" % (a, t)
return f(*args, **kwds)
new_f.func_name = f.__name__
return new_f
return check_accepts
And then use as:
#accepts(Decimal)
def calculate_price(monthly_item_price):
...
You can modify my decorator to achieve what you want.
You can just check if the input is correct in the function:
def my_request(protocol_type: str, url: str):
if protocol_type in ('http', 'https'):
# Do x
else:
return 'Invalid Input' # or raise an error
Why not use a Literal for the method argument?
def my_request(protocol_type: Literal["http","https"], url: str):
Use an if statement that raises an exception if protocol_type isn't in a list of allowed values :
allowed_protocols = ['http', 'https']
if protocol_type not in allowed_protocols:
raise ValueError()

How can I add context to an exception in Python

I would like to add context to an exception like this:
def process(vals):
for key in vals:
try:
do_something(vals[key])
except Exception as ex: # base class. Not sure what to expect.
raise # with context regarding the key that was being processed.
I found a way that is uncharacteristically long winded for Python. Is there a better way than this?
try:
do_something(vals[key])
except Exception as ex:
args = list(ex.args)
if len(args) > 1:
args[0] = "{}: {}".format(key, args[0])
ex.args = tuple(args)
raise # Will re-trhow ValueError with new args[0]
The first item in ex.args is always the message -- if there is any. (Note for some exceptions, such as the one raised by assert False, ex.args is an empty tuple.)
I don't know of a cleaner way to modify the message than reassigning a new tuple to ex.args. (We can't modify the tuple since tuples are immutable).
The code below is similar to yours, except it constructs the tuple without using an intermediate list, it handles the case when ex.args is empty, and to make the code more readable, it hides the boilerplate inside a context manager:
import contextlib
def process(val):
with context(val):
do_something(val)
def do_something(val):
# assert False
return 1/val
#contextlib.contextmanager
def context(msg):
try:
yield
except Exception as ex:
msg = '{}: {}'.format(msg, ex.args[0]) if ex.args else str(msg)
ex.args = (msg,) + ex.args[1:]
raise
process(0)
yields a stack trace with this as the final message:
ZeroDivisionError: 0: division by zero
You could just raise a new exception:
def process(vals):
for key in vals:
try:
do_something(vals[key])
except Exception as ex:
raise Error(key, context=ex)
On Python 3 you don't need to provide the old exception explicitly, it will be available as __context__ attribute on the new exception object and the default exception handler will report it automatically:
def process(vals):
for key in vals:
try:
do_something(vals[key])
except Exception:
raise Error(key)
In you case, you should probably use the explicit raise Error(key) from ex syntax that sets __cause__ attribute on the new exception, see Exception Chaining and Embedded Tracebacks.
If the only issue is the verbosity of the message-amending code in your question; you could encapsulate it in a function:
try:
do_something(vals[key])
except Exception:
reraise_with_context(key=key) # reraise with extra info
where:
import inspect
import sys
def reraise_with_context(**context):
ex = sys.exc_info()[1]
if not context: # use locals from the caller scope
context = inspect.currentframe().f_back.f_locals
extra_info = ", ".join("%s=%s" % item for item in context.items())
amend_message(ex, extra_info)
raise
def amend_message(ex, extra):
msg = '{} with context: {}'.format(ex.args[0], extra) if ex.args else extra
ex.args = (msg,) + ex.args[1:]

What does except really do in Python?

I'm really new in Python and a have no experience with exceptions but I've read all the documentation and couldn't find an answer ... so I'm looking for a deeper view in except's semantics.
When we have for example:
try:
x = 2
except GreaterThanOne:
print("The value is greater than one")
In this case I want the message to be printed.Is there a way for the GreaterThanOne class(exception) to be defined to raise when the entered value is greater than one ?
Ok, let me be more specific ...
Every error raises by a specific rule which should be add in the error attributes, am I right ?
For example:
try:
myvalue = x / y
except ZeroDivisionError:
print("Some error message printed ...")
So when I use this code and enter for y to be 0 the exception ZeroDivisionError will raise ... Can I for example redefine ZeroDivisionError to raise like this but if y is set to be ... 2 or 3 or any other value ?
Input:
x = 10
y = 2
try:
myvalue = x / y
except ZeroDivisionError:
print("division by 2")
Output: division by 2
Here's an example that should help you understand. Run this in your Python interpreter and watch how the exception is raised and caught (or not caught) when you call set_val(2).
# Defining our Exception subclass:
class GreaterThanOne(Exception):
pass
# The global value we pretend to care about:
val = 0
# Function to set a value but possibly raise our new Exception
def set_val(new_val):
if new_val > 1:
raise GreaterThanOne("%d > 1" % new_val)
val = new_val
# Catching exception:
try:
set_val(0)
set_val(1)
set_val(2)
except GreaterThanOne:
print "Whoops - one of those values was greater than one"
# Not catching exception:
set_val(0)
set_val(1)
set_val(2)
set_val(3)
an try-except block catches exception in this block.
try:
#some stuff here
except ExceptionClass as e:
#Exception handling here
the class after the except keyword indicates which kind of exception you want to catch. Usually you give a specific class, like ValueError or KeyError. You can also use the Exception class, to catch any exception. Because all the other exceptionclasses inhert from Exception.
so if you want to use this construct, an exception needs to be raised, Either by a function / method you call, or you raise it yourself with the raise keyword.
like this:
try:
raise KeyError('Just for test')
except KeyError as e:
#Exception handling here
The try except doesn't automagically inspect the whole code between it, it just looks for exceptions... Or to be more specific, it looks for those exceptions you tell it to look for.
Of course you can also inspect the exception instance.
try:
raise KeyError('Just for test')
except KeyError as e:
print e.args
For more information, please see:
http://docs.python.org/2/tutorial/errors.html

Categories

Resources