First-class objects in Python As a beginner, in order to cement my knowledge of functions in Python, I’ve prepared this Jupyter notebook First-Class Functions. The code in it is working, but before moving on to closures and decorators I would like you to comment on my code please.

def cube(x):
    return x ** 3

cube_two = cube(2)    # Call the function, set cube_two to the value returned

print(cube)            # It says cube is a function
print(cube_two)       # The value stored in cube_two

# &lt;function cube at 0x010E71E0&gt;
# 8


Returned values as list elements and dict values:

nums = [4, cube(4), cube(4) / cube(4) ** 0.5]    # returned values as list elements
print(nums)

cube_dict = {'two_cubed': cube(2), 'five_cubed': cube(5)}   # as dict values
print(cube_dict)

# [4, 64, 8.0]
# {'two_cubed': 8, 'five_cubed': 125}


Setting a variable to the cube() function:

make_cube = cube    # Take out those parentheses

print(cube)
print(make_cube)    # make_cube points to the cube() function

# &lt;function cube at 0x010E71E0&gt;
# &lt;function cube at 0x010E71E0&gt;


the variable make_cube can be used as a function, like so:

print(cube)
print(make_cube(4))

# &lt;function cube at 0x010E71E0&gt;
# 64


map taking a function (int) as an argument:

str_nums = ['1', '2', '3']
one, two, three = map(int, str_nums)  # similar to [int(n) for n in str_nums] which is pythonic

print(str_nums)
print([one, two, three])

# ['1', '2', '3']
# [1, 2, 3]


reduce is another example: it applies a function of two arguments cumulatively to the items of an iterable.

from functools import reduce

cumu_diff = reduce(lambda x, y: x - y, [3, 2, 1])    # cumulative difference
print(cumu_diff)

# 0


Creating a map-like function (well, kind of).

def map_it(func, seq):    # func (without parentheses) as an argument
    for x in seq:
        yield func(x)

str_nums = ['1', '2', '3']
one, two, three = map_it(int, str_nums)    # same as map(int, str_nums)

print([one, two, three])

# [1, 2, 3]


Returning a function from another function:

def greet(name):

    def print_message():        
        print("Hello, " + name + "!")     # print_message() gets the name from greet()

    return print_message                  # returning the function, not calling it

greet_et = greet('E.T.')    # greet_et points to print_message()

print(greet)
print(greet_et)
print(greet_et())

# &lt;function greet at 0x01090C90&gt;
# &lt;function greet.&lt;locals&gt;.print_message at 0x010B36A8&gt;
# Hello, E.T.!
# None


Here's another example.

def make_paragraph(tag):

    def wrapper(text):
        return '&lt;{0}&gt;{1}&lt;/{0}&gt;'.format(tag, text)

    return wrapper


para_one = make_paragraph('p')

print(para_one('This is a small paragraph.'))   # returns '&lt;p&gt;This is a small paragraph.&lt;/p&gt;'

#&lt;p&gt;This is a small paragraph.&lt;/p&gt;
There are a few general improvements I would suggest for readability and clarity and apply throughout.


Use docstring to document your functions, modules and classes.
Since Python 3, you can now use type hints for your function and method signatures, and use static code analysis tools. This also makes the code easier to read for humans.




Applying these to your cube function, you would get something like this:

def cube(number: int) -&gt; int:
    """Return the cube of a number,
    i.e. the number to the power of 3."""
    return number ** 3


Note that I renamed the x argument to a more descriptive number argument. What the above means is that the function expects number to be an integer, and that the function should return an integer. 

Another problem I see with this function is that it does not check for correct input. If you type cube("hello") then the program just crashes with an error TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'. It would be better to handle the error. There are at least two ways to do this.

Using isinstance:

def cube(number: int) -&gt; int:
    if not isinstance(number, int):
        return TypeError("{0} is not an integer".format(number))
    return number ** 3


Using try and except:

def cube(number: int) -&gt; int:
    try:
        return number ** 3
    except TypeError as err:
        return TypeErrorTypeError("{0} is not an integer".format(number))


Using the latter, you could also just except TypeError as err and return err which will return the normal error listed above. 

You can also use assert which will instead raise an AssertionError if the type is incorrect, for instance assert(isinstance(number, int)). This is particularly useful when setting up tests for your code. 

I personally find that the first method is simpler to use, and you can use it to check multiple types at once, but the latter is the standard way to handle something that may cause an error. Choose whichever is more appropriate for the situation. 



Applying the same ideas to map_it, this is slightly more complicated since the type hints have to be imported from typing library. Again, I renamed x to a more descriptive name elem.

from typing import Callable, Sequence, Iterable

def map_it(func: Callable, seq: Sequence) -&gt; Iterable:
    """Applies a given function to a sequence and yields the result."""
    for elem in seq:
        yield func(elem)


You could also test using instanceof here if you liked, which may or may not be necessary depending on your use cases. 



Your greet function can also have type errors, if you call it with a non-string type, e.g., greet(42). The simple fix for this (and I recommend to use it any time you need to put variables/arguments into strings) is to use .format() instead of +'ing string and values. 

from typing import Callable

def greet(name: str) -&gt; Callable:
    """Returns a callable greeting with given name."""
    def print_message() -&gt; None:
        """Callable to print a greeting to stdout."""
        print("Hello, {0}!".format(name))
    return print_message




One more thing, I noticed your code is peppered with # line comments, in general comments like that should only be used to clarify or explain why something is done, in most cases they are not needed as the code speaks for itself.