Search⌘ K
AI Features

Functions Related Tips

Explore techniques for safe function handling in Python, including when and how to call functions correctly. Understand the dangers of eval() and the safer alternative ast.literal_eval(). Learn to distinguish type() from isinstance() for effective type checking and discover why avoiding redefining built-in names is crucial. This lesson equips you with practical safety tips to write secure, maintainable Python functions.

Call that function

A function identifier is a reference to a function, and acts as a claim that the function has been defined and exists somewhere in the interpreter’s memory. When we mention an existing function’s identifier on the command line, the interpreter confirms that the function exists. It also tells us the address of the function in the RAM, but that’s not our concern:

Python 3.8
def add1(x):
return x + 1
print(add1)

The interpreter doesn’t call a function unless we ask it to call the function using the function call operator, also known as the parentheses:

Python 3.8
print(add1(10))

Not calling a function isn’t a syntax or runtime error. There’s nothing wrong with merely referring to the function. In most cases, though, that’s not desirable. This is the correct code:

Python 3.8
def add1(num):
num=num+1
return num
result = add1(10)
print(result)

The code below is potentially correct. It provides an alternative identifier to an existing function:

Python 3.8
increment = add1
result = increment(10)
print(result)

The code below likely incorrect, though:

Python 3.8
result = add1
print(result)

Don’t eval()

The built-in function eval(expr), is the most misused and dangerous function in the Python standard library. The function takes the expr string and evaluates it as a Python expression. Essentially, eval() is a Python interpreter in disguise. We can construct Python expressions on the fly and immediately evaluate them:

Python 3.8
message = 'Hello, world!'
command = 'print(message)'
eval(command)

What could go wrong? Imagine that the command wasn’t produced by our program that is based on a carefully constructed algorithm but was entered by the user. For example, say we develop a program that allows users to calculate arithmetic expressions.

Let’s run the code below for more understanding.

command = input('Enter the expression (only constants, no variables alowed) you would like to calculate: ')
eval(f'print({command})')
Evaluating the arithmetic expression using eval()

Seeing how it works, the user becomes troublesome.

Warning: Don’t attempt to run this code fragment. This function will delete all the files and directories.

os.system('rm -rf /')

The command above will return 0. The zero displayed at the command prompt confirms the worst expectations: the user just removed the content of the root directory. It’s being exaggerated a bit, but the results may be devastating. The problem with eval(expr) is that, in general, it isn’t trivial to ensure the safety of expr before evaluating it.

So, unless we can guarantee that expr won’t cause any harm, avoid eval(expr).In the majority of cases, there is never a strong enough reason to use it. Even when we’re sure that expr is safe, avoid eval(expr) nonetheless. An alternative would be its less problematic cousin, ast.literal_eval() as we’ll see below.

Parse with literal_eval()

What if we want to read a Python data structure such as a list, dictionary, or set, prepared by str() or repr()?

As seen above, eval() isn’t safe, so should we write our own parser for intrinsic Python data types?

Python actually has a function that, reciprocates repr() to some extent. It’s called ast.literal_eval() and was originally designed to implement custom parsers. As a reminder of those intentions, ast refers to an abstract syntax tree.

The ast.literal_eval(expr) function converts the expr expression to a Python object as long as expr is a string representation of bytes, or a number, tuple, list, dictionary, set, boolean, None, or another string.

Let’s call the ast.literal_eval() function below.

Python 3.8
print(ast.literal_eval('["Mary", None, 23, 3.14, {"pet": "lamb"}]'))

The function can evaluate simple arithmetic expressions as long as they don’t go beyond addition and subtraction.

Let’s see how it works.

Python 3.5
print(ast.literal_eval('3+3-2'))

The literal_eval() won’t work for arithmetic operations other than addition and subtraction.

An attempt to run literal_eval('3+3*2') will throw an error of a malformed string.

Python 3.5
print(ast.literal_eval('3+3*2'))

The function was designed to be safe, which makes it a feasible alternative to eval(). The built-in safety features also make it useless to parse more advanced expressions, such as multiplication, list/dictionary indexing, and function calls. Safety has a price.

Check for range

The built-in range() function returns a namesake object. A range object is an iterable and we can use it as the sequence in a for loop. A range object isn’t an iterator, so we can’t get its next element by calling next(), and we can iterate over a range many times without consuming it.

A range object isn’t a range in the algebraic meaning of the word; range(x,y) is not the same as [x,y). The closest other data structure that describes the inner world of a range is a set of numbers. Similar to a set, a range is discrete (but ordered).

An immediate consequence of this observation is that the operator, in, checks if its left operand is one of the discrete numbers in the range. It doesn’t check if the left operand is numerically greater than or equal to the start of the range and smaller than the end of the range:

Let’s check 5 in range(10)

Python 3.8
print(5 in range(10))

(Because 5 is a number between one and nine.)

Let’s see the output if a number isn’t in a specific range.

Python 3.8
print(5.5 in range(10))

(Because 5.5 is not one of those numbers in the integer number’s range (1-10)) The number 5.5 is not within the integer number’s range.

The correct way to check if x is in an algebraic range [a,b) is to use the comparison operators.

Let’s try chain comparison now!

Python 3.8
print(0 <= 5.5 < 10)

Distinguish type() and isinstance()

We call a function polymorphic if it accepts different types of arguments. Many built-in functions in Python are polymorphic, such as len() and print(). Writing a polymorphic function in Python is simple. We need to know the type of the argument and then, based on the type, apply some operation to the parameter. For example, we can improve the len() function. We can’t use the original len() to measure the length of a number. We may quite reasonably argue that the length of a number is its absolute value and write a function my_len(x) that returns either the absolute value or the count of items in x, depending on the type of x.

There are two ways to retrieve the type of an object: by calling the type(obj) and isinstance(obj,type_s) functions. The type() function returns the type of the argument:

Let’s see how it works!

Python 3.8
print(type(int))
print(type(type(int)))
print(type(type(type(int))))

Integers (int()) are a class, too. In fact, every object in Python belongs to a class. Even the type of an object is a class of its own.

Note: Instances of a class, int, have the int.numerator (equal to the number itself) and int.denominator (always equal to 1) attributes because Python also supports rational numbers. Rational numbers are provided in the module, fractions. A rational number p/qp/q can be expressed as Fraction(p,q).

The isinstance() function takes an object and a type or a list of types and checks if the object belongs to one of the listed types. It has a sister function issubclass(), that checks if a class is a subclass of another class or a list of classes (remember, everything in Python is a descendant of the class object):

Python 3.8
print(isinstance(1,int))
print(isinstance(1,float))
print(isinstance(1,object))
print(isinstance(1,numbers.Integral))

An integer literal, like 1, isn’t merely an int but also a numbers.Integral. This would be possible only if int is a direct or indirect subclass of numbers.Integral.

Note: The int class is a subclass of numbers.Integeral but the reverse isn’t true.

Python 3.8
print(issubclass(int, numbers.Integral))
print(issubclass(numbers.Integral, int))

The major difference between type() and isinstance() is that the former reports the most immediate class of its parameter, while the latter checks if the parameter belongs to one of the classes either directly or indirectly. Suppose we want to implement that my_len() thing:

def my_len(x):
    if type(x) in (str, list, tuple, map, dict):
        return len(x)
    if type(x) in (bool, int, float, complex):
        return abs(x)
    raise Exception('x has no length')

Let’s create MyList, a trivial subclass of list, and apply this function to a MyList object. MyList, for any practical purpose, is simply a list:

Python 3.8
class MyList(list):
def __init__(self,x):
super().__init__(x)
l = MyList([1, 2, 3])
print(len(l))

Let’s call my_len(l) and observe the output.

Python 3.8
def my_len(x):
if type(x) in (str, list, tuple, map, dict):
return len(x)
if type(x) in (bool, int, float, complex):
return abs(x)
raise Exception('x has no length')
class MyList(list):
def __init__(self,x):
super().__init__(x)
l = MyList([1, 2, 3])
print(my_len(l))

That’s because MyList is a kind of a list but not exactly the list; it’s a subclass.

A better version of my_len() manages the class hierarchy:

Python 3.8
def my_len(x):
if isinstance(x, (str, list, tuple, map, dict)):
return len(x)
if isinstance(x, (bool, int, float, complex)):
return abs(x)
raise Exception('x has no length')
class MyList(list):
def __init__(self,x):
super().__init__(x)
l = MyList([1, 2, 3])
print(my_len(l)) # Call again

We should use isinstance() when we need to know if an object belongs to a class or any superclass of it because future actions depend on some features possibly possessed by the superclasses. We should use type() when we need to know the ultimate precise type of the object.

Don’t call the list list

Python comes pre-equipped with a collection of about 150 built-in functions. There’s almost nothing special about them, except that they didn’t belong to any module long ago. Now, they belong to the builtins module, though technically, that module is a part of the core Python and is imaginary.

Unfortunately, built-in functions in Python are not protected against vandalism. The most common way to vandalize them is to redefine their identifiers. That happens when we want to give a simple, clear name to a new variable. For example, why not call a new list list?

list = [2, 4, 6, 8, 10]

This statement works as long as we or anyone else reusing our code doesn’t attempt to create another list using the class constructor:

Python 3.8
anotherList = list(1, 2, 3)

List isn’t a constructor anymore. In fact, it’s not even a function. It’s a list, just as we wanted it to be before we changed the rest of our code.

To avoid trouble, don’t redefine any identifier from the builtins module. Not sure if a prospective identifier is a built-in function or variable? Import the module and check:

Python 3.8
import builtins
print('list' in dir(builtins))
print('mylist' in dir(builtins))

As a rule, adding the prefix, “my”, to an identifier makes that identifier ours because no built-in identifier starts with “my”:

Python 3.8
print([x for x in dir(builtins) if x.startswith('my')])

Quiz on functions

Technical Quiz
1.

What is the output of the following code?

import ast
ast.literal_eval('2*4-2-2')
A.

4

B.

An error

C.

6


1 / 4