Creating a New Type

We’ve just looked at object oriented programming, talked about attributes and types, and now we’re going to discuss how to create your own new types.

type

In Python, when you create a new type, you need to call the type function. That makes sense – type itself is a type, and we call types to create new instances of them.

(In practice, you’ll hardly ever call types. We’ll talk about the class statement later. The class statement calls the type function for you.)

Normally you call type() with one argument, expecting to get back the type of the argument you passed in. If you were to call it with three parameters, it will return a new type, ready to create new instances. The type looks like a function, just like the other types.

The three parameters are:

  • name: The name of the new type. You probably want this to match your variable name used to hold it.

  • bases: A tuple of other types that this is derived from. (We’ll talk about inheritance in a separate video.)

  • dict: A dictionary of the type attributes or methods and the values to which they should correspond.

Let’s create a new type:

my_type = type('my_type', (), {})

o = my_type()
type(o) # my_type
isinstance(o, my_type) # True
isinstance(o, int) # False

Now you have a new type.

The nice thing about your type is you can assign arbitrary attributes to it:

o.a = 5
o.b = (3,4)

Although we typically don’t use this method to create new types, it’s important to understand what it is and how it works. Later on we’ll dive into metaclasses, which require you understand what is really going on behind the scenes.

Namespaces and Types and Instances

In preparing you to fully understand what is going on when attributes are looked up, I want you to first get used to the idea that there are multiple namespaces each instance is involved with:

  • The namespace of the object itself.

  • The namespace of its type.

  • The namespaces of the types the type is derived from.

When you do an attribute lookup, it is going to look first in the object’s namespace. If it can’t find it, it will look into the namespece of its type.

For instance:

my_type.c = 100
o.c # --> 100, using the type's namespace

o.c = 50
o.c # --> 50, using the instance's namespace

del my_type.c
o.c # --> 50

del o.c
o.c # --> AttributeError

In the near future, I’ll go over how Python’s attribute access really works when you take metaclasses, inheritance, and special methods into consideration. For now, this simplistic model is enough, and most of the time it is true.

The Class Statement

I’m going to give you the class statement now:

class name(bases): SUITE

What this does is it first executs the SUITE in its own namespace. This namespace is not unlike the def statement for function definition, except it is called right away. After the suite executes, the namespace is sent to the type function as the dict parameter. Finally, the result is assigned to a new variable named name.

The bases are entirely optional. You can leave it out altogether if there are no bases for the class. It will give you the base class for all classes by default. We’ll talk more about bases in a later video.

I encourage you to use TitleCase with your class names. This is tradition more than anything else.

What should you do in a class suite?

  • If you want an empty class (which is not as uncommon as you might think), just put the pass statement. It does nothing except take up space.

  • If you are going to fill it out later, put in .... (Yes, it’s valid Python.)

  • You can add class attributes which will end up attached to the type.

  • You can define functions, which will also end up attached to the type.

The functions you define in a class statement are called methods. The reason why is Python does a little magic when you access an attribute through and instance of a type that is a function attached to the type. It binds the instance to the method such that when the method is called, the instance is passed as the first parameter.

Let me give an example:

class Greeter:
    greeting = "Hello!"

    def say_hello(self):
        print(self.greeting)

Greeter.say_hello() # TypeError
Greeter.say_hello(Greeter) # "Hello!"

a = Greeter()
m = a.say_hello
m() # --> prints "Hello!"
a.greeting = "Good morning!"
a.say_hello() # --> prints "Good morning!"

A few things here will help clear up any confusion you might have.

  1. The class statement’s suite is run when the class statement is run. In this case, it only defines two variables: greeting and say_hello.

  2. When accessing say_hello through the Greeter type, it behaves exactly like a normal function. You need to specify an object with the greeting attribute in order for the function to work. (Greeter fulfills that requirement – so you can call it like the above.)

  3. When you create an instance of the type, and then access say_hello. instead of requiring the first parameter, the object itself is passed in!

The __init__ Method

The first, an probably most important, special method I will tell you about is __init__. This is commonly called the constructor although I don’t like that name. It’s just “init” to me.

__init__ is called whenever a new instance is created, and the new instance is always passed in as the first parameter. If you specified additional positional or named arguments to the type function, then these will be passed along. Let’s give an example:

class Greeter:

    def __init__(self, greeting):
        self.greeting = greeting

    def say_hello(self):
        print(self.greeting)

In this code, everything behaves the same except for when we go to creat an instance. Greeter() will not work – you didn’t specify greeting. However, Greeter('Hello!') will work fine. It returns a new Greeter with the greeting set to ‘Hello!’.

Don’t forget that you can use *args, **kwargs and default values for the parameters to __init__. It’s just a function, like any other.

Let’s play with this a bit:

a = Greeter("Hello!")
b = Greeter("G'Day!")
c = Greeter("Hola!")
a.say_hello() --> "Hello!"
b.say_hello() --> "G'Day!"
c.say_hello() --> "Hola!"
c.greeting --> "Hola!"
c.greeting = "Buon giorno!"
c.say_hello() --> "Buon giorno!"

self

I have used self as the first parameter to methods, and it’s something I will keep doing, as this is very, very standard in Python.

In rare, rare cases you might want to use something else. These rare cases typically involve classmethods or very special descriptors. I’ll go over that when we cover those topics.

Class Attributes

  • __name__: The class name

  • __module__: The module name where the class was defined.

  • __dict__: The dictionary containing the namespace.

What next?

  • Learn about inheritance and super.

  • Learn about descriptors.

  • Learn about classmethod and staticmethod.

  • Learn the special methods and how they are used!

  • Learn about metaclasses.

With the above, you’ll know everything there is to know about Python’s OOP.

Why Classes?

Before I let you go, I want to talk a little bit about when and why we use classes in Python.

As a general rule of thumb, I avoid classes whenever possible. Let me describe a few situations where people typically use classes but they don’t need to.

  1. A singleton. A singleton is a class that is meant to only ever have one instance, like the NoneType type for the None object. Instead of a singleton, try using a module. (We’ll cover modules later.)

  2. A collection of functions. Sometimes people use a class to bring functions closer together. You’ll see that the “self” parameters aren’t even used in these functions, and so it’s typically better to just include the functions in their own module.

  3. A struct. A “struct” is a C/C++ concept for a new type which is really just an amalgam of other types. It looks a lot like a class except it has no methods, only attributes. If you find yourself writing a class with no methods, then you’re probably better off storing the data in a dictionary.

Here is my guide for how to write code, and whether to use classes.

  1. Try to write everything in a single file as a recipe without functions. Maybe I’ll put everything in a “main” function.

  2. If you need to make function calls, then write those functions.

  3. If you find that you’re passing the same data over and over again to similar functions, then perhaps moving that data into a module is the right idea.

  4. If you find you’re passing the same data over and over again, but you have multiple sets of that data that you’d like to distinguish, then it seems to be the ideal case for a class.