Advanced Python
In-Class Exercise Solutions, Session 7

Notes if you are using Jupyter Notebook: to call exit() from a notebook, please use sys.exit() (requires import sys); if a strange error occurs, it may be because Jupyter retains variables from all executed cells. To reset the notebook, click 'Restart Kernel' (the circular arrow) -- this will not undo any changes made.

CLASS VARIABLES / ATTRIBUTES

Ex. 7.1 Identify the object and attribute in each of the bottom 4 code lines.
import json
import sys

mylist = [1, 2, 3, 4]               # list, [1, 2, 3, 4]
mystr = 'hello'                     # str, 'hello'
fh = open('../pconfig.json')        # 'file' object

# identify the object and attribute in each of the below

                    #  OBJECT          ATTRIBUTE

mylist.append(5)    #  mylist (list)   append (method)

mystr.upper()       #  mystr (str)     upper (method)

json.load(fh)       #  json (module)   load (function)

sys.argv            #  sys (module)    argv (list)

Every object has attributes, which are names defined for the object or the object's class/type. Each attribute points to another object, often a method or function but can also be string, list, etc. We can think of attributes as variables that belong to the class and are accessible through the object. Anytime we see this.that, we are looking at [object].[attribute]. This means we should be able to identify the object and its type, as well as the object type that the attribute is pointing to.

 
Ex. 7.2 Without calling any of the below attributes, print the type of each.
import json
import sys

mylist = [1, 2, 3, 4]               # list, [1, 2, 3, 4]
mystr = 'hello'                     # str, 'hello'
fh = open('../pconfig.json')        # 'file' object

# print the type of each

print(type(mylist.append))          # <class 'builtin_function_or_method'>

print(type(mystr.upper))            # <class 'builtin_function_or_method'>

print(type(json.load))              # <class 'function'>

print(type(sys.argv))               # <class 'list'>

Note that we are not calling these attributes (which would require parentheses), we are simply printing them. This is why instead of seeing an uppercase string, etc., we are seeing printed the object that each attribute is pointing to.

 
Ex. 7.3 What exception does this code cause Python to raise and why?
mystr = '1, 2, 3, 4, 5'        # str, '1, 2, 3, 4, 5'

mystr.append(5)                # AttributeError:  'str' object has no attribute 'append'


# AttributeError is similar to a NameError
print(myyystr)                 # NameError:  'myyystr' is not defined

Anytime we use an attribute, Python must look up the attribute to see what object it is pointing to (in the same way that Python looks up variable names to see what object they are pointing to). If the attribute is not available through the object, then Python raises an AttributeError indicating that that attribute does not exist for that object type. This is very simliar to a NameError, which indicates that a variable name has not been defined. An attribute is simply a name pointing to an object. In this way it can be seen as very similar to a variable, which is also a name pointing to an object. The difference is that an attribute is stored in a class or instance, but a variable name is stored the program itself (as a global or local variable).

 
Ex. 7.4 Use dir() on one or more of these objects and examine the result.
import sys                           # module object
import json                          # module object

mylist = ['a', 'b', 'c']                 # list object
mydict = {'a': 1, 'b': 2, 'c': 3}        # dict object
mystr = 'hello'                          # str object
myint = 55                               # int object

print(dir(mylist))

    # ['__add__', '__class__', '__contains__', '__delattr__',
    # '__delitem__', '__dir__', '__doc__', '__eq__', '__format__',
    # '__ge__', '__getattribute__', '__getitem__', '__gt__', ... ]

This shows that every object can be inspected to see attributes that are available to it. The list of strings returned from dir() shows attributes that are defined in the object, in its class and in any classes from which it inherits.

Since dir() produces a list of strings, we can print or modify items in any way convenient:
# loop through list of attributes and print on separate line
for attr in dir(sys):                       # str, '__breakpointhook__'
    print(attr)


# use a list comprehension to show only attribute names with 'version' in the name
attrs = [ attr for attr in dir(sys) if 'version' in attr ]

print(attrs)                 # ['api_version', 'hexversion', 'version', 'version_info']
 
Ex. 7.5 Use hasattr() in an if statement to check for an attribute within an object; if True, use getattr() to retrieve and print the attribute.
import sys                           # retrieve the 'version' attribute

if hasattr(sys, 'version'):          # bool, True
    print(getattr(sys, 'version'))   # 3.8.5 (default, Sep  4 2020, 02:22:02)
                                     # [Clang 10.0.0 ]


mylist = ['a', 'b', 'c']             # retrieve the 'append' attribute

if hasattr(mylist, 'append'):        # bool, True
    print(getattr(mylist, 'append')) # <built-in method append of list object at 0x10b1e7f00>

Note that the attributes are specified as strings. The built-in functions getattr() and hasattr() allow us to check or work with the attributes of an object, without having to use object.attribute syntax. Why would we want to use a string to access an attribute rather than just by using attribute syntax? Occasionally we may wish to access the attribute programatically, which means we wouldn't be able to hard-code the attribute in our program, since it would be retrieved or identified at runtime. Since hasattr() and getattr() use string attributes, the attribute name can be specified using a value generated by the program (for example, using a string attribute that came from dir()).

 
Ex. 7.6 Do is a class object; the class has a cval attribute assigned as a class variable. Investigate this class object's attributes with the following tools:

  1. regular attribute lookup (i.e., .cval)
  2. dir()
  3. Do.__dict__.keys()

class Do:

    cval = 5                 # class variable:  int, 5

    def dothis(self):        # class variable:  function
        print('done!')

print(Do.cval)               # 5
print(Do.dothis)             # <function Do.dothis at 0x11baf3050>

print(dir(Do))
    # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
    #  '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
    #  '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
    #  '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
    #  '__repr__', '__setattr__', '__sizeof__', '__str__',
    #  '__subclasshook__', '__weakref__', 'cval', 'dothis']

print(Do.__dict__.keys())
    # dict_keys(['__module__', 'cval', 'dothis', '__dict__',
    #            '__weakref__', '__doc__'])

.__dict__ is a special attribute available on most objects. It is a dictionary of the object's attributes, pairing attribute names with the object associated with each attribute. Here is the first place we should note that the attribute name keys in .__dict__ are a subset of attribute names returned from dir() -- in other words, every key in .__dict__ can also be found in dir(), but dir() has keys not found in .__dict__. Why is .__dict__ a subset of dir()? Because dir() takes into account inheritance -- the ability for a class or object to access attributes in "parent" classes (classes from which this class inherits). Thus .__dict__ shows all attributes in an object, but dir() shows all attributes available to an object.

The .__dict__ attribute dictionary is an alternative way to access attributes in an object:
print(Do.__dict__['cval'])# 5

print(Do.__dict__['dothis'])# <function Do.dothis at 0x11baf3050>
 
Ex. 7.7 obj is an instance of the Do() class; an attribute has been set in the instance, and the class also has its own attribute, set as a class variable. Investigate the instance and class attributes with the following tools:

  1. regular attribute lookup (i.e., [instance].oattr and [instance].cval)
  2. class lookup (i.e., [class].cval)
  3. dir()
  4. obj.__dict__

class Do():

    cval = 10                   # class variable: int, 10

    def __init__(self):         # self:  'Do' instance
        self.oattr = 500        # set .oattr attribute of self to 500

    def dothis(self):           # self:  'Do' instance
        print('done!')


obj = Do()                      # 'Do' instance

print(obj.oattr)                # 500       (instance attribute)
print(obj.cval)                 # 10        (class attribute through instance)

print(Do.cval)                  # 10        (class attribute through class)

print(obj.__dict__)             # {'oattr': 500}  (instance attrs only)

print(dir(obj))
    # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
    # '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
    # '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
    # '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
    # '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
    # '__weakref__', 'cval', 'dothis', 'oattr']

Make special note that the .__dict__ dictionary for an instance contains only those attributes that were set in the instance itself. So we can reaffirm the essential difference between .__dict__ and dir(): .__dict__ shows the object's own attributes, whereas dir() shows all attributes available to the object, including those provided by inheritance. In the case of an instance, dir() shows all attributes in the object, its class and any parent classes.

 
Ex. 7.8 class What inherits from class Do. Investigate the What instance attributes with the following tools:

  1. regular attribute lookup (i.e., .cval)
  2. dir()
  3. Do.__dict__.keys()

class Do:

    dovar = 5                     # class variable/attribute:  int, 5

    def dothis(self):             # class variable/attribute:  function
        print('done!')


class What(Do):

    whatvar = 10                  # class variable/attribute:  int, 10

    def __init__(self):
        self.instval = 500        # set .instval attribute of self to 500

z = What()                        # 'What' instance

# accessing all attributes through the instance
print(z.instval)                  # 500 (instance attribute)
print(z.whatvar)                  # 10  (class attribute)
print(z.dovar)                    # 5   (class attribute of parent class)


# dir() reflects all available attributes, including
#              those of class and parent class
print(dir(z))

  # ['__class__', '__delattr__', '__dict__', '__dir__',
  #  '__doc__', '__eq__', '__format__', '__ge__',
  #  ... continues until ...
  #  'dovar', 'dothis', 'instval', 'whatvar']


# .__dict__ shows only attributes for the instance
print(z.__dict__)

  # {'instval': 500}

Here we should reaffirm the "order of attribute lookups" or "cascading attribute lookups". When looking up an attribute, an instance will look: 1. in itself 2. in its class 3. in any parent classes

 
Ex. 7.9 Again with class What inheriting from class Do, compare the results from dir() and the results from the .__dict__ attribute of the class. (No need to create an instance, simply work with the What class object.)
class Do:

    d = 5                      # class variable/attribute:  int, 5

    def dothis(self):          # class variable/attribute:  function
        print('done!')


class What(Do):
    w = 10                     # class variable:  int, 10

    def whatever(self):        # self: 'What' instance
        print('chill!')

print(dir(What))
    # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
    # '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
    # '__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
    # '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
    # '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
    #  'd', 'dothis', 'w', 'whatever']

print(What.__dict__.keys())
    # dict_keys(['__module__', 'w', 'whatever', '__doc__'])

The results should show you that What.__dict__ includes attributes specific to What and that dir(What) also includes attributes defined in Do. Added to this are attributes defined in the ultimate parent class, object, from which all objects inherit.

 
Ex. 7.10 Again with class What inheriting from class Do, view the .__bases__ attribute of both the What class and the Do class. (Again, no need to create an instance.)
class Do:
    dval = 5

class What(Do):
    wval = 10

print(What.__bases__)     # (Do,)
print(Do.__bases__)       # (object,)

The .__bases__ attribute allows us to ask a class what class(es) it inherits from. The value is a tuple with a list of parent classes. (The comma after a single item is required to notate a one-item tuple - without it, the parens would be just plain parentheses.) Note that What inherits from Do as expected, but Do inherits from object.

If we look at the .__doc__ docstring attribute of object, we can see what the built-in type object is intended to be:
print(object.__doc__)# The most base type

'base' is another word for parent. Another way to say "The most base type": "The ultimate parent of all classes".

 
Ex. 7.11 Compare dir() of Do to dir() of object. Is one a subset of the other?
class Do:
    pass

print(dir(Do))
   # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
   #  '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
   #  '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
   #  '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
   #  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
   #  '__weakref__']


print(dir(object))
   # ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
   #  '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
   #  '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__',
   #  '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
   #  '__sizeof__', '__str__', '__subclasshook__']


# show what is in Do that is not in object

print(set(dir(Do)).difference(dir(object)))
  # {'__weakref__', '__module__', '__dict__'}


# show what is in object that is not in Do

print(set(dir(object)).difference(dir(Do)))
  # set()

In short, dir(object) is a subset of dir(Do) because Do inherits from object. So Do has access to all of object's attributes, as well as its own. Questions may arise about the meaning and purpose of some of these attributes. When such a question arises, print the reference and see what you can learn! It shouldn't be considered important to understand every attribute, although you should be able to at least recognize most of them eventually.

 
Ex. 7.12 Make class Do inherit from the builtin class list. Try to use a Do instance as if it were a list.
class Do(list):

    d = 5

    def dothis(self):
        print('done!')

obj = Do()                   # 'Do' instance

obj.dothis()                 # prints done!

obj.append(5)                # calls list 'append' method
obj.append(10)               # calls list 'append' method

print(obj)                   # [5, 10]

Do acts like a list! because it inherits all of list's attributes. Do also has its own attributes and functionality.

 
Ex. 7.13 Look at the .__bases__ attribute of the exception class ValueError; see if you can follow the chain of bases all the way up to object.
print(ValueError.__bases__)         # (Exception,)
print(Exception.__bases__)          # (BaseException,)
print(BaseException.__bases__)      # (object,)

All roads lead to object! Some Python classes are part of a multi-tiered hierarchy. Here, ValueError inherits from Exception and Exception inherits from BaseException, which inherits from object. You can see the complete hierarchy of exception types at the bottom of this page.

 
Ex. 7.14 Examine the return value from the following function calls: vars().keys(), locals().keys() (called from inside the function) and dir(builtins).

Note: to run this cell properly on Jupyter, please click 'restart kernel' (the circular arrow button near the top of the notebook) to clear variables; otherwise, vars() will hold all variables defined in each cell up to this point.

import builtins

a = 5                               # int, 5
b = [1, 2, 3]                       # list, [1, 2, 3]

print('== vars().keys() ==')
print(vars().keys())
print()

def do():
    lvar = 500                      # int, 500
    print('== locals().keys() ==')
    print(locals().keys())
    print()

do()

print('== dir(builtins) ==')
print(dir(builtins))
== vars().keys() ==
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__',
           '__annotations__', '__builtins__', '__file__', '__cached__',
           'builtins', 'a', 'b'])

== locals().keys() ==
dict_keys(['lvar'])

== dir(builtins) ==
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning',
 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning',
 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError',
 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError',
 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError',
 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError',
 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError',
 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError',
 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError',
 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError',
 '__build_class__', '__debug__', '__doc__', '__import__', '__loader__',
 '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin',
 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod',
 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir',
 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format',
 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id',
 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open',
 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed',
 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum',
 'super', 'tuple', 'type', 'vars', 'zip']

vars() returns a dict of variables that are in the global namespace, and locals() returns variables that are in a function's local namespace. They make it possible to find and use a program's variables programmatically. builtins is a module containing all builtin variables (functions like len() and round() and all exception types, such as AttributeError or ValueError).

 
Ex. 7.15 Run the code to view the '.__doc__' attribute for the class and for the dothis() method. Now add "docstrings" (floating triple-quoted strings) just inside both the class and def statements. Run the program again to see how the .__doc__ attributes have changed.
class Do:
    """ a class for doing something magnificent """
    d = 5

    def dothis(self):
        """ this function is somethin' """
        print('done!')


print(Do.__doc__)              # a class for doing something magnificent
print(Do.dothis.__doc__)       # this function is somethin'

The .__doc__ for our own classes and methods defaults to None, but when we place docstrings inside each, they are picked up by Python and placed in the .__doc___ attribute for each object. This mechanism makes it possible for us to add readable documentation that can also be imported and read by a "docreader", or program that builds documentation based on our docstrings.

 
Ex. 7.16 Compare dir(Do) with inspect.getmembers(Do).
import inspect

class Do:

    d = 5                        # class variable:  int, 5

    def dothis(self):
        print('done!')

print(dir(Do))
print()
print(inspect.getmembers(Do))

  # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
  #  '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
  #  '__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
  #  '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
  #  '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
  #  '__weakref__', 'd', 'dothis']

  # [('__class__', <class 'type'>),
  # ('__delattr__', <slot wrapper '__delattr__' of 'object' objects>),
  # ('__dict__', ...,

The thing to notice here is that the .getmembers(Do) returns a list of 2-item tuples, each of which has a first item that is an attribute name that can be found in the list returned from dir(Do). Thus inspect.getmembers() reads the same attributes as dir() but includes the object pointed to by each attribute as well - this can be very convenient for perusing an object's attributes and accessing its attribute values.

 
Ex. 7.17 Check to see whether Do.d and Do.dothis are callable.
class Do:

    d = 5                    # class variable:  int

    def dothis(self):        # class variable:  function
        print('done!')

print(callable(Do.d))        # False
print(callable(Do.dothis))   # True

If we're interested in finding functions or methods defined within a module or class, we can use the callable() built-in function to see whether an attribute can be called as a function or method.

 

"MAGIC" METHODS

Ex. 7.18 Allow a class to "print" itself. Define a __str__(self) method that returns a string. Now print the object.
class Value:
    def __init__(self, val):
        self.aaa = val                       # set .aaa to 10 in self

    def getval(self):
        return self.aaa

    def __str__(self):                       # called when instance is printed
        return f'Value object: {self.aaa}'

mynum = Value(10)          # 'Value' instance/object

print(mynum)               # calls mynum.__str__()
                           # prints Value object: 10

When an object is printed, it must produce a string value that represents the object. Strings "5", "'hello'" and "['a', 'b']" are meant to represent int, str, and list. As designers of a class, we can decide how an object represents itself when printed or converted to string. The __str__() method must return a string -- this string is the value that will be printed.

 
Ex. 7.19 Allow an object to respond to subscripting. Define a __setitem__(self) method that takes two arguments (besides self) and prints the two arguments. Now attempt to subscript the object.
class Value:
    def __init__(self, val):
        self.aaa = val                   # set .aaa to 10 in self

    def __setitem__(self, key, val):     # called when subscript is used (below)
        print(f'called __setitem__({key}, {val})')

mynum = Value(10)                        # 'Value' instance

mynum['a'] = 55                          # calls mynum.__setitem__(self, 'a', 55)
                                         # prints "called __setitem__(a, 55)"

Like __str__(), __setitem__() responds to a specific operation on the object -- in this case, assigning a value to a subscript. The key and value are sent to the method. Inside the method we can do anything we want with the key and value.

 
Ex. 7.20 Replace getval() with __getitem__(). The method should take one argument (besides self) and return the .aaa attribute. Now attempt to subscript the object.
class Value:
    def __init__(self, val):
        self.aaa = val                 # set .aaa attribute to int, 10 in self

    def __getitem__(self, val):        # val:  str, 'whaeva'
        return self.aaa                # return 10

mynum = Value('10')                    # 'Value' instance

val = mynum['whaeva']                  # int, 10

Similar to __setitem__(), __getitem__() responds to subscripting (but when we're reading from the subscript, and not assigning to it). The argument to the method is the subscript being passed. In this particular implementation, we're not doing anything with the subscript value 'whaeva' but in a normal implementation we would use it with the object to access a particular value from the object.

 

INHERITING FROM BUILTINS

Ex. 7.21 Make IntList inherit from list and then treat it like a list.
class IntList(list):  # IntList inherits from list
    pass              # pass means an empty block

x = IntList()         # IntList isntance

x.append(5)           # calls list.append() method
x.append(3)           # calls list.append() method
print(x)              # calls list.__str__() method:  [5, 3]

You may note that the class IntList is completely empty. All of is functionality is coming from list. In other words, all of the attributes of list, including its methods, have been inherited and are accessible through the IntList object.

 
Ex. 7.22 Add an append() method to IntList to specialize the method. Inside the method, only print a message; don't try to perform the append.
class IntList(list):

    def append(self, item):     # replaces list.append
        print(f'now appending {5}!')

x = IntList()                   # IntList instance

x.append(5)                     # now appending 5!  (calls IntList.append())
x.append(3)                     # now appending 3!

print(x)                        # []
 
Ex. 7.23 Call list.append() from within the inheriting class. From inside the IntList .append() method, call the append() method on the object by calling it on the parent class. (Don't call it on self or you will set up an endless loop.)
class IntList(list):
    def append(self, item):              # extends list.append()
        list.append(self, item)          # calling list.append() and
        print(f'now appending {item}!')  # doing something more

x = IntList()                            # IntList instance

x.append(5)                              # now appending 5!
x.append(3)                              # now appending 3!

print(x)                                 # [5, 3]

This solution calls list.append(self, item), i.e. it passes self to the parent class .append() method, which performs the append on our IntList object. We must not call self.append(item) because that would invoke the IntList.append() method, which would then call self.append(item) again... this sets up a "recursive loop" of method calls that will go on until Python hits the RecursionError recursive loop circuit-breaker, when 1000 recursions are detected.

 

TRAPPING AND RAISING EXCEPTIONS

Ex. 7.24 (Review) Trap the exception. First, run the script to see what exception is raised. Then, use a try/except statement to trap the error. In the except block, set xi to 1.
x = input('please enter an integer: ')        # str, 'hello' (sample input)

try:
    xi = int(x)           # attempts int('hello'); raises ValueError exception
except ValueError:        # traps ValueError
    xi = 1                # sets variable to default

print(f'{xi} * 2 = {xi * 2}')

PLEASE DO NOT USE except: BY ITSELF! That works, but we must always specify the exception we are expecting.

 
Ex. 7.25 (Review) Trap the exception (2). Again, determine the exception type that will occur if the user types in a key that doesn't exist in the dict. Then use the try/except to trap the exception (PLEASE DO NOT USE except: BY ITSELF). If the exception is raised, assign None to val.
keydict = {'a': 1, 'b': 2, 'c': 3}               # dict, {'a': 1, 'b': 2 ... }

x = input('please enter a key for this dict: ')  # str, 'XX' (sample input)

try:
    val = keydict[x]         # attempts keydict['XX']; raises KeyError
except KeyError:             # traps KeyError
    val = None               # sets val to a default value

print(f'the value for {x} is {val}.')

PLEASE DO NOT USE except: BY ITSELF. You must almost always specify the exception you are trapping -- otherwise, you could trap other exceptions without realizing what they are or how they happened.

 
Ex. 7.26 Trap the exception, then raise a new one. Inside the except block, raise another KeyError, or any existing exception that you choose.
keydict = {'a': 1, 'b': 2, 'c': 3}                 # dict, {'a': 1, 'b': 2 ... }

x = input('please enter a key for this dict: ')    # str, 'XX' (sample input)

try:
    val = keydict[x]              # attempts keydict['XX']; raises KeyError
except KeyError:                  # traps KeyError
    raise KeyError(f'"{x}" is not a key in this dict!')  # raise new KeyError

print(f'the value for {x} is {val}.')

Here we're raising the same exception, but this time it's with a message that we choose (that is more descriptive of what's happening in our code).

 
Ex. 7.27 Without any reason, raise an exception. Choose your favorite exception! You don't have one? Then raise ZeroDivisionError.
raise ZeroDivisionError('division by zero, pal!')    # raises ZeroDivisionError

As demonstrated, you don't need a reason to raise an exception: we can raise one for any reason. The raise statement creates this error condition, and no other events or problems are needed.

 
[pr]