Understand Type, Class, and Scope of Python

Tetsuya Hirata
6 min readJun 5, 2021

--

Photo by Timothy Dykes on Unsplash

Python itself has the two parts of the code which are built-in code or not. The built-in code means that the code is written inside an interpreter based on C. These type, class, and scope as the python core components have built-in code and not built-in code.

This might lead to the confusion for python learners to understand type, class, and scope because built-in code is difficult for them to find the written places and understand C code.

So, the article try to clearly identify what type, class, and scope are and how they work.

Prerequisite Knowledge

  • Object has the two meanings in python formal documents such as abstract representation of data and class.
  • Object has data types, id, value as abstract representation of data.
  • Object itself is class.
  • Class is the way to define data types.
  • Class always inherits object as first argument but dose not need to be explicitly written.
  • Class is the another representation of a group of dictionaries.
  • This is because each object and variable of namespace is stored in __dict__ as dictionary.
  • A namespace is the place of objects which is composed of the three scopes(global, local, built-in).
  • Special methods are represented by double underscore and used for overriding built-in methods and classes.
  • When a variable name is used in Python code, it is resolved by looking it up in multiple nested scopes.
  • Python looks up variable references by searching them in code block scope’s dictionary.

Overview of Type and Class

This image is created by Jesse Tetsuya.
This image is created by Jesse Tetsuya.

User-Defined Class

In Python, everything is object. Class is the way to define data types.

By writing class and making it instance, the above code define the data type as UserDefinedClass which is bonded to the top level module (__main__).

>>> class UserDefinedClass:
... pass
...
>>> c = UserDefinedClass()
>>> type(c)
<class '__main__.UserDefinedClass'>

Before making it instance, see what the data type of UserDefinedClass object by using type() and trace what class is inherited from by using __bases__.

>>> type(UserDefinedClass)
<class 'type'>
>>> UserDefinedClass.__bases__
(<class 'object'>,)
>>> type(UserDefinedClass.__bases__[0])
<class 'type'>
>>> UserDefinedClass.__bases__[0].__bases__
()

UserDefinedClass inherits object and object dose not inherit anything, so empty tuple is returned.

Built-in (data) Types and Built-in Classes

These are defined by class and the types of (data) type are type. Type is also type defined by class. You can check the below code.

>>> for t in int, float, dict, list, tuple:
... print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
>>> type(1)
<class 'int'>
>>> type(type)
<class 'type'>

If you tried to customize built-in classes, you can write like the below. Let’s see it step by step.

  1. This is the built-in class definition of float() that I identified based on debugging.
class Float:
def __new__(cls, x=None):
if x is None:
return 0.0
else:
return x.__float__()
def __float__(self):
...
...
...
...

2.1. Defined new method but __float__ of Float has the logic to check if the object is float or not.

>>> class CustomizedFloat:
... def __init__(self, value):
... self.value = value
... def __float__(self):
... return self.value * 10
...
>>> f = CustomizedFloat(2)
>>> float(f)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: CustomizedFloat.__float__ returned non-float (type int)
>>> f = CustomizedFloat(2.0)
>>> float(f)
20.0

2.2. Override the built-in class definition of float()

>>> class CustomizedFloat(float):
... def __init__(self, value):
... self.value = value
... def __float__(self):
... return super().__float__()*10
...
>>> f = CustomizedFloat(2)
>>> float(f)
20.0

Tips: what is x.__float__() ?

>>> dir(f)
['__abs__', '__add__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dict__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getformat__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__round__', '__rpow__', '__rsub__', '__rtruediv__', '__set_format__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__weakref__', 'as_integer_ratio', 'conjugate', 'fromhex', 'hex', 'imag', 'is_integer', 'real', 'value']
>>> f.__float__()
20.0

Built-in Functions

When built-in classes are used as functions, these are like the below.

bool(), bytearray(), bytes(), complex(), dict(), float(), frozenset(), int(), list(), object(), range(), set(), slice(), str(), tuple()

Below is a list of the types that are built into Python. Extension modules (written in C, Java, or other languages, depending on the implementation) can define additional types. Future versions of Python may add types to the type hierarchy (e.g., rational numbers, efficiently stored arrays of integers, etc.), although such additions will often be provided via the standard library instead.

Some of the type descriptions below contain a paragraph listing ‘special attributes.’ These are attributes that provide access to the implementation and are not intended for general use. Their definition may change in the future. (3.2. The standard type hierarchy)

>>> len
<built-in function len>
>>> type(len)
<class 'builtin_function_or_method'>

This might be difficult to check behaviors of built-in methods, so let’s see it by overriding built-in methods with special methods and changing the operand behaviors.

>>> class CustmizedAdd:
... def __init__(self, value):
... self.value = value
... def __add__(self, other):
... return self.value * other.value
...
>>> c1 = CustmizedAdd(10)
>>> c2 = CustmizedAdd(20)
>>> c1 + c2
200

Built-in methods are defined in object class and you can use them in any scope. So, the above code inherited object and override object.__add__.

For example, object.__add__ can be used like the below code.

>>> 1.__add__
File "<stdin>", line 1
1.__add__
^
SyntaxError: invalid syntax
>>> a = 1
>>> a.__add__
<method-wrapper '__add__' of int object at 0x107bff930>
>>> a.__add__(3)
4

Class can be defined by type()

class CustmizedClass1:
x = 1
CustmizedClass1()
-> <__main__.CustmizedClass1 at 0x10d4ca640>
CustmizedClass2 = type('CustmizedClass2', (object,), dict(x=1))
CustmizedClass2()
-> <__main__.CustmizedClass2 at 0x10d4cae20>

Class can be instances by two ways like the below.

class C:
def f(self, value):
return value

1) self binds x

x = C()
z = C.f(x,1)
type(z)
<class 'int'>

2) When the instance methods is called, __func__(__self__, *args) as special methods are also called. You do not need to write self.

x = C()
y = x.f(1)
type(y)
<class 'int'>

Overview of Scope

Python try to look up the variable from local scope, global scope, and built-in scope in order.

This image is created by Jesse Tetsuya.
This image is created by Jesse Tetsuya.

You can find what are in built in scope like the below command.

$ python3
Python 3.9.2
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> 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']

You can see each object and variable of namespace is stored in __dict__ as dictionary by using locals() and globals().

$ python
Python 3.9.2
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> y = 2
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1, 'y': 2}
>>> def func1():
... z =3
... print(locals())
... return z
...
>>> def func2():
... z = 4
... print(locals())
... return z
...
>>> def func3():
... print(globals())
... return "variables in global scope"
...
>>> func1()
{'z': 3}
3
>>> funct2()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'funct2' is not defined
>>> func2()
{'z': 4}
4
>>> func3()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1, 'y': 2, 'func1': <function func1 at 0x107e09280>, 'func2': <function func2 at 0x107e09310>, 'func3': <function func3 at 0x107e093a0>}
'variables in global scope'

--

--

Tetsuya Hirata

Software engineer working mostly at the intersection of data science and engineering. @JesseTetsuya