An Introduction to Graphics in Python

Learning objectives

In this code along, we will give a short introduction to graphics in Python, which will help us in future code alongs to visualize the Game of Life and other cellular automata. We will then use this introduction to make a simple but stunningly beautiful drawing that showcases our artistic prowess.

Code along summary

Setup

To complete this code along as well as other code alongs relying on graphics in this and following chapters, you will use a popular Python graphics library called pygame, which will help us draw objects and generate images.

First, install pygame by opening a command line terminal and executing the command pip3 install pygame (on macOS/Linux) or pip install pygame (on Windows).

In python/src, create a folder drawing, then create main.py inside it with the following contents. Note that we import the pygame package at the top.

import pygame

def main():
    print("Drawing a snowperson.")


if __name__ == "__main__":
    main()

Understanding Pygame: our library for drawing

The pygame library allows us to create a graphical window for our programs. We will cover more about object-oriented programming in a future chapter, but for now, think of a pygame window as a rectangular screen with a specified width and height, similar to the screen you are reading this on. The width and height are measured in pixels, where each pixel is a tiny point on the screen that can be colored individually.

In the RGB color model, every rectangular pixel on a computer screen emits a single color formed as a mixture of differing amounts of the three primary colors of light: red, green, and blue (hence the acronym “RGB”). The intensity of each primary color in a pixel is expressed as an integer between 0 and 255, inclusive, with larger integers corresponding to greater intensities.

A few colors are shown in the figure below along with their RGB equivalents; for example, magenta corresponds to equal parts red and blue. Note that a color like (128, 0, 0) contains only red but appears duskier than (255, 0, 0) because the red in that pixel is less intense.

A collection of colors along with their RGB codes. This table corresponds to mixing colors of light instead of pigment, which causes some non-intuitive effects; for example, yellow is formed by mixing equal parts red and green. The last six colors appear muted because they only receive half of a given color value compared to a color that receives 255 units. If all three colors are mixed in equal proportions, then we obtain a color on the gray scale between white (255, 255, 255) and black (0, 0, 0). Source: Excel at Finance.

The functions from pygame that we will need in this and the next code along are listed below. However, a best practice for using Python libraries is to refer to the online API provided here to understand functions and objects in the library: https://www.pygame.org/docs/. We will say more about these functions as they are needed. (Links are provided for specific documentation of each function below for those who are interested.)

  • pygame.Surface.fill(): Fills the Surface with a solid color. (link)
  • pygame.draw.circle(): draw a circle. (link)
  • pygame.draw.rect(): draw a rectangle. (link)
  • pygame.draw.line(): draw a line. (link)
  • pygame.init(): initialize all imported pygame modules. (link)
  • pygame.Surface(): pygame object for representing images. (link)
  • pygame.image.save(): save an image to file. (link)
  • pygame.quit(): uninitialize all pygame modules. (link)

Creating a blank canvas

In main(), we initialize pygame by calling pygame.init(). Even though we won’t open a window, pygame still needs to set up its internal modules before we can create surfaces or draw anything.

def main():
    print("Drawing a snowperson.")

    # Initialize pygame but don't open window
    pygame.init()

Next, we create a canvas that we can draw on by constructing a new pygame.Surface. We will make our canvas 1000 pixels wide and 2000 pixels tall.

def main():
    print("Drawing a snowperson.")

    # Initialize pygame but don't open window
    pygame.init()

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

Next, we will define some colors using RGB values. black = (0, 0, 0) represents the absence of light, white = (255, 255, 255) is full light for all three channels, and red = (255, 0, 0) is just red. Pygame functions will take in these 3-int tuples for color parameters.

def main():
    print("Drawing a snowperson.")

    pygame.init()

    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)

Next, we paint the entire surface with black by calling surface.fill(black).

def main():
    print("Drawing a snowperson.")

    pygame.init()

    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)

    # Fill canvas with black
    surface.fill(black)
Note: surface.fill() is a method (a function that is part of the Surface object) that fills the entire surface with the input color. More information on how to define these functions for objects will be discussed in a future code along.

We can convert our surface to an image by saving it to a PNG so that it can be viewed outside of Python. We call the function pygame.image.save() to produce this PNG, which we will call "fun.png".

After we have drawn the face, we call pygame.quit() to shut down all Pygame modules and release all resources. This step is important to prevent memory leaks and save memory and properly close Pygame’s internal systems.

def main():
    print("Drawing a snowperson.")

    pygame.init()

    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)

    surface.fill(black)

    pygame.image.save(surface, "fun.png")

    pygame.quit()

Although we have not yet written code to draw the snowperson, let’s run our code so that we can see what is produced.

STOP: In a new terminal window, navigate into our directory using cd python/src/drawing. Then run your code by executing python3 main.py
(macOS/Linux) or python main.py (Python).

After running your code, you should see the achievement of modern art shown below appear as fun.png in your python/src/drawing directory.

A thrilling black rectangle.

The graphics coordinate system

Before adding parts of our snowperson to the drawing, we need to understand the pygame coordinate system. In the Cartesian plane that we are accustomed to working with in mathematics, increasing x-coordinates extend to the right, and increasing y-coordinates extend upward.

In the classic Cartesian plane, x-coordinates increase to the right, and y-coordinates increase upward.

However, graphics uses a different standard that dates to the foundation of computing. Many graphics packages (pygame included) use the standard of viewing the top left corner of a window as an “origin”, with increasing x-coordinates extending to the right, and increasing y-coordinates extending downward.

In graphics, x-coordinates increase to the right, and y-coordinates increase downward.

Drawing the snowperson

Rather than placing all our drawing code inside def main(), we will call a function named draw_snowperson(). This function will take our surface as input so that it knows which canvas to draw on, as well as the colors that we plan to use. We update def main() to include the call to draw_snowperson().

def main():
    print("Drawing a snowperson.")

    pygame.init()

    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)

    surface.fill(black)

    draw_snowperson(surface, black, white, red)

    pygame.image.save(surface, "fun.png")

    pygame.quit()

We will now implement draw_snowperson(). This function will take the surface as input (our canvas) along with the colors we want to use. Inside draw_snowperson(), we will call three helper functions: draw_head(), draw_middle(), and draw_bottom(). For now, we will comment out the latter two functions so that we can run the code as soon as we finish draw_head().

def draw_snowperson(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    draw_head(surface, black, white, red)
    # draw_middle(surface, black, white)
    # draw_bottom(surface, black, white)

Drawing the head

We will now add a head to surface, which will appear as a white circle near the top of the canvas. To do so, we will call the pygame.draw.circle() function. Remember when your geometry teacher told you that a circle is defined by its center and its radius? We know that you don’t remember it, but it is nevertheless true. We will now use this fact by calling pygame.draw.circle(), which takes four parameters as input:

  • surface (pygame.Surface): the surface you want to draw on.
  • color (tuple or pygame.Color): the color of the circle, e.g. (255, 0, 0) for red.
  • center (tuple of two ints): the (x, y) coordinates of the circle’s center.
  • radius (int): the radius of the circle in pixels.

We want the head to lie in the top part of the rectangle, halfway across from left to right. Because the canvas is 1000 pixels wide, we know that the x-coordinate of the circle’s center should be 500. In order to have the same amount of space on the top, left, and right, let’s make its y-coordinate 500 as well; that is, the circle’s center will be 400 pixels from the top. As for the radius, let’s set it to be 180 pixels. As a result, we will call pygame.draw.circle(surface, white, (500, 400), 180).

def draw_head(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 400), 180)

When we run our code again, we obtain the head below as fun.png.

Adding a big round head to our drawing.

Adding facial features to the head

Now that we have a head, it is time to give our snowperson a personality (or at least some facial features). We will add eyes, a nose, eyebrows, and a mouth.

We will start with a nose, which we will draw as a small black circle slightly below the eyes. The nose’s x-coordinate stays at 500 (we respect symmetry), and we’ll place it at y-coordinate 410. We will give it radius 10 pixels.

def draw_head(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 400), 180)

    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 410), 10)
Do you think my nose is too small?

Next we add two circular black eyes. We will place them slightly above the center of the head. Since our head is centered at (500, 400), let’s put the eyes at y-coordinate 360. We will move them 60 pixels left and right of the center line, so their x-coordinates will be 440 and 560. We will set their radii to 15 pixels so that our snowperson can actually see.

def draw_head(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 400), 180)

    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 410), 10)

    # add eyes (black circles)
    pygame.draw.circle(surface, black, (440, 360), 15)
    pygame.draw.circle(surface, black, (560, 360), 15)

Running our code produces the unsettling stare shown below.

Our snowperson is shy.

Now for a mouth. We will make it a red rectangle so that we can use pygame.draw.rect(). This function takes (x, y, width, height), where (x, y) is the top-left corner of the rectangle. Let’s make the mouth 140 pixels wide and 20 pixels tall, and place it at x = 430 and y = 450.

def draw_head(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 400), 180)

    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 410), 10)

    # add eyes (black circles)
    pygame.draw.circle(surface, black, (440, 360), 15)
    pygame.draw.circle(surface, black, (560, 360), 15)

    # add mouth (red rectangle)
    pygame.draw.rect(surface, red, (430, 450, 140, 20))
He is smiling.

Finally, we will add eyebrows using pygame.draw.line(). This function draws a line segment between two given points with thickness equal to a third parameter. We’ll draw two thick black lines above the eyes on an angle.

def draw_head(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
    red: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 400), 180)

    pygame.draw.circle(surface, black, (440, 360), 15)
    pygame.draw.circle(surface, black, (560, 360), 15)

    pygame.draw.circle(surface, black, (500, 410), 10)

    pygame.draw.rect(surface, red, (430, 450, 140, 20))

    # add eyebrows (thick black lines)
    pygame.draw.line(surface, black, (420, 310), (470, 340), 10)
    pygame.draw.line(surface, black, (580, 310), (530, 340), 10)

Running the program now produces the completed head below.

The snowperson is judging you.

Drawing the middle

Next, we will draw the middle circle of the snowperson below the head. In addition to another larger circle, we will add two buttons as small black circles.

def draw_middle(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 800), 260)

    pygame.draw.circle(surface, black, (500, 760), 12)
    pygame.draw.circle(surface, black, (500, 860), 12)

Drawing the bottom

Finally, we draw the bottom circle of the snowperson and add three buttons down the center.

def draw_bottom(
    surface: pygame.Surface,
    black: tuple[int, int, int],
    white: tuple[int, int, int],
) -> None:
    pygame.draw.circle(surface, white, (500, 1300), 360)

    pygame.draw.circle(surface, black, (500, 1200), 12)
    pygame.draw.circle(surface, black, (500, 1320), 12)
    pygame.draw.circle(surface, black, (500, 1440), 12)

Full program

Putting everything together, we uncomment draw_middle() and draw_bottom() in draw_snowperson() our complete main.py is shown below.

def draw_snowperson(surface, black, white, red):
    draw_head(surface, black, white, red)
    draw_middle(surface, black, white)
    draw_bottom(surface, black, white)

When we run our code, we obtain the final snowperson below.

Allow me to (re)introduce myself.

Looking ahead

Now that we understand a bit more about drawing, we are ready to apply what we have learned to draw cellular automata. Please join us in the next code along to do so!

Scroll to Top
Programming for Lovers banner no background
programming for lovers logo cropped

Join our community!

programming for lovers logo cropped
Programming for Lovers banner no background

Join our community!