0% found this document useful (0 votes)
8 views648 pages

Hands-On Python With 50 Exercises

Hands-On Python is an intermediate-level book by Musa Arda that provides a practical introduction to Python programming through 50 exercises, 2 projects, 2 assignments, and a final exam. The book covers essential topics such as exception handling, modules, file operations, and object-oriented programming, with a focus on hands-on coding and real-world applications. Supplementary resources, including code files and quizzes, are available for download on GitHub.

Uploaded by

lemleme
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views648 pages

Hands-On Python With 50 Exercises

Hands-On Python is an intermediate-level book by Musa Arda that provides a practical introduction to Python programming through 50 exercises, 2 projects, 2 assignments, and a final exam. The book covers essential topics such as exception handling, modules, file operations, and object-oriented programming, with a focus on hands-on coding and real-world applications. Supplementary resources, including code files and quizzes, are available for download on GitHub.

Uploaded by

lemleme
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 648

HANDS-ON

PYTHON
with 50 Exercises, 2 Projects, 2 Assignments & Final
Exam

INTERMEDIATE

Musa Arda
Hands-On Python with 50 Exercises, 2 Projects, 2 Assignments
& Final Exam: Intermediate

By Musa Arda

Copyright © 2021 Musa Arda.

All rights reserved. No portion of this book or its supplementary


materials may be reproduced in any form without permission from
the publisher, except as permitted by U.S. copyright law.

For permissions contact: [email protected]


Contents
Preface
1. Introduction
2. IDE - PyCharm Basics
3. Exception Handling
4. Modules & Packages
5. Format Operations
6. File Operations
7. Project 1 - Working with PDF & CSV Files
8. Assignment 1 - Working with PDF & CSV Files
9. OOP
10. Project 2 - Movie Library with Tkinter
11. Assignment 2 - Movie Library with Tkinter
12. Final Exam
13. Conclusion
Preface
About This Book
This book is an in-depth and activity-based introduction to the
intermediate level topics of Python programming. It follows a
step-by-step practical approach by combining the theory of the
language with the hands-on coding exercises including quizzes,
projects, assignments and exams.
We begin by setting up the development environment, the
PyCharm IDE. Then we cover Exception Handling, Modules &
Packages, Format Operations and File Operations in Python.
Further we cover one of the most important topics in computer
programming which is OOP (Object Oriented Programming). We
build two big projects that are the PDF & CSV Operations and the
Movie Library. Especially in the Movie Library we build a real
world project which you can use as a reference in your
programming life. You will also have assignments after each
project. And finally we have the final exam which will let you to
test your Python level.
By the end of the book, you will learn almost all of the
intermediate level concepts of Python in great detail by writing
thousands of lines of code. All the supplementary resources (code
files, quizzes, assignments, final exam etc.) are available for
download at the Github repository. The link for the repository is
provided in the book.
This is the second book in Hands-On Python Series. And here is
what you will find in this book:
Theory: In each topic, we will cover all the Theoretical Details
with example coding.
Coding Exercises (50 questions): At the end of each chapter,
we will have Coding Exercise, Quizzes, of 10 questions.
Projects (2): We will have 2 Projects in this book. You will
learn how to apply Python concepts on real world problems.
Assignments (2): After each project, you will have an
Assignment. These assignments will let you build the project from
scratch on your own.
Final Exam: At the end of this book you will have the Final
Exam. It is a multiple choice exam with 20 questions and a limited
duration. The exam will let you to test your Python level.
About Hands-On Python Series
This is the second book in our Hands-On Python Series. And it
covers the intermediate level topics. So we assume that you already
know the introductory concepts of Python programming like;
Variables, Functions, Conditional Statements, Loops, Strings,
Lists, Dictionaries, Tuples, Sets and Comprehensions. If you don’t
feel comfortable with these topics, you are strongly recommended
to finish the first book in our series, which is the Beginner book.
Here, you can find it.[1]
About the Author
Musa Arda has Bachelor’s degree from Industrial Engineering
in 2007, and he has been working as a Software Developer for
more than 14 years. He uses many programming languages and
platforms like; Python, C#, Java, JavaScript, SAP ABAP, SQL,
React, Flutter and more.
He creates online learning content and writes books to share his
experience and knowledge with his students. His main purpose is to
combine theory and hands-on practice in his teaching resources.
That's why there are hundreds of programming exercises, quizzes,
tests, projects, exams and assignments in all of his courses and
books. He is dedicated to help his students to learn programming
concepts in depth via a practical and exiting way.
How to Contact Us
Please feel free to get in contact with the author. To comment or
ask technical questions about this book, you can send email to
[email protected].
1. Introduction
Who Is This Book For?
The goal of this book is to help students to learn Python
programming language in a hands-on and project-based manner.
With its unique style of combining theory and practice, this
book is for:
people who are new to computer science and programming
people who are new to Python
people who want to gain solid Python Skills
people who want to learn Python by doing Projects, Quizzes,
Coding Exercises, Tests and Exams
people who want to learn Python for Machine Learning, Deep
Learning, Data Science and Software Development in general
anyone who wants to learn real Python
What Can You Expect to Learn?
The purpose of this book is to provide you a good introduction
to the intermediate topics of Python programming. In general, you
will gain solid programming skills and grasp the main idea of
software development. In particular, here are some highlights on
what you can you expect to learn with this book.
You will:
learn & master Programming Fundamentals, Coding
Algorithms and Computer Science Concepts
go from Intermediate to Advanced level in Python with hands-
on approach
do exercises on fundamental topics of Python with 5 Quizzes
and 50 Coding Exercises
build 2 Real-World Project with Python and do 2 Assignments
related to these projects
take the Final Exam on Python with 20 questions to assess
your learning
build Python applications with PyCharm and master it
learn Python fundamentals you need for Machine Learning,
Deep Learning, Data Science and Application Development
gain solid and profound Python Programming skills needed for
a Python career
Outline of This Book
In Chapter 1, you will get the basics of the book. You will learn
about this books approach to Python programming and how to get
most out of it.
In Chapter 2, you will meet PyCharm, our IDE (Integrated
Development Environment) for this book. You will learn all the
basics of PyCharm and how to do debugging with it. You will also
learn the base interpreter and interpreter configuration.
In Chapter 3, you will learn how to handle exceptions in your
code. Exception Handling is one of the most important topics in
programming. You will learn and practice the try-except-else-
finally structure in detail.
In Chapter 4, you will learn Modules & Packages in Python.
You will also define your custom modules and use them in your
programs.
In Chapter 5, will see Format Operations in detail. You will
learn how to format the Strings in Python. The ways will be Format
Operator, String.Format, f-Strings and Template Strings.
In Chapter 6, you will learn the File Operations in Python. You
will read and write files, delete, move and copy them, create
folders and learn how to search over files & folders.
In Chapter 7, you will have your first project. You will work on
PDF and CSV files in Python. You will read and write CSV & PDF
files, create new PDF’s and merge them.
In Chapter 8, you will have your first assignment. It is based on
the PDF & CSV Project in Chapter 7 but with missing code
segments in it. Don’t worry, you will have the guidelines to
complete the project on your own.
Chapter 9 is on one of the main building blocks of
programming which OOP. OOP stands for Object Oriented
Programming and it’s crucial if you want to develop real world
applications. You will learn and practice OOP and its core concepts
in great detail.
In Chapter 10, you will have the second project. It is mainly on
OOP concepts like classes, objects and inheritance. You will use
Python’s built-in Tkinter Module and develop a Movie Library
with it.
Chapter 11 is your second assignment. This time you will try
develop the Movie Library project in Chapter 10 on your own by
completing the missing code segments.
Chapter 12 is your Final Exam. Now it’s time to assess your
Python level. You will have an exam of 20 questions on the topics
you learned in this book. The Final Exam will give you the chance
to see what you learned and which topics you should repeat.
Chapter 13 is the Conclusion. You will finalize this book and
will see how you should proceed for the next step.
Conventions Used in This Book
The following typographical conventions are used in this book:

Italic
Indicates new URLs, email addresses, filenames, and file
extensions.

Bold
Indicates new terms and important concepts to pay attention.

Constant width
Used for string literals and programming concepts within
paragraphs.

Constant width bold


Used for program elements within paragraphs, such as variable
or function names, data types, statements, and keywords.

We will write our code in the code cells. Here is an example


code cell with the cell number as 7:

Figure 1-1: A code cell example used in this book


Using Code Examples
You can find all the supplementary resources for the book (code
files, quizzes, assignments, final exam etc.) available for download
at https://github.com/musaarda/python-hands-on-book-
intermediate.
You & This Book
This book is designed in a way that, you can learn and practice
Python. At each chapter you will learn the basic concepts and how
to use them with examples. Then you will have a quiz of 10
questions at the end of the chapter. First you will try to solve the
quiz questions on your own, then I will provide the solutions in
detail. You will have projects after each block of core concepts.
And after every project you will an assignment to test your
understanding. This will be your path to learn real Python.
Before we deep dive into Python, I want to give you some tips
for how you can get most out of this book:
Read the topics carefully before you try to solve the quizzes
Try to code yourself while you are reading the concepts in the
chapters
Try to solve quizzes by yourself, before checking the solutions
Read the quiz solutions and try to replicate them
Code the projects line by line with the book
Do the assignments (seriously)
Do not start a new chapter before finishing and solving quiz of
the previous one
Repeat the topics you fail in the Final Exam
Learning takes time, so give yourself enough time digest the
concepts
2. IDE - PyCharm Basics
A Brief History of Python
Python is a widely used and general-purpose, high-level
programming language[2]. Its design philosophy emphasizes code
readability and its syntax allows programmers to express concepts
in fewer lines of code. Python is dynamically-typed and garbage-
collected. It supports multiple programming paradigms, including
procedural, object-oriented and functional programming.
It was initially designed by Guido van Rossum in the late
1980s and developed by Python Software Foundation. Python was
named after the BBC TV show Monty Python's Flying Circus.
In 1991, Van Rossum published the code labeled version 0.9.0.
Then in 1994, Python reached version 1.0.
Python 2.0 was released in 2000, which started the Python 2.x
family that has been used widely for almost a decade. The final
release, Python 2.7.18, occurred on 2020 and marked the end-of-
life of Python 2. As of January 1st, 2020 no new bug reports, fixes,
or changes will be made to Python 2, and Python 2 is no longer
supported.
Python 3.0 (also called "Python 3000" or "Py3K") was released
on December 3, 2008 and it was a major, backwards-incompatible
release. Since Python 3.0 broke backward compatibility, much
Python 2 code does not run unmodified on Python 3. Although
some old projects still use Python 2, the preferred approach is using
Python 3. As of this writing, the latest version is 3.10, but every
code we write in this book should run on Python 3.6 or any later
version.
Python Installation
To install Python, open the official Python web site, python.org,
in your web browser and navigate to the Downloads tab.

Figure 2-1: Official Python web site

Here, you can download the current version for your operating
system. Once you download the executable file then it is very
straightforward to install Python.
Figure 2-2: The latest version on python.org

Run the executable installer to start installation. You will see


the initial screen for setup. The recommended installation includes
pip, IDLE and documentation. Pip is the package installer for
Python. We will see it later in this book. IDLE is the built-in IDE
for Python development, but we will not be using it. We will be
using PyCharm.
Figure 2-3: Python setup wizard

Make sure you select the Install launcher for all users and
Add Python 3.10 to PATH checkboxes. After selecting these
checkboxes you can click Install Now button. It will initialize the
setup process. It may take several minutes to finalize depending on
your computer.
Figure 2-4: Python setup progress
When your computer finishes installation you will see the final
screen saying Setup was successful.
Figure 2-5: Final setup screen

There is an important option on the final screen. It is about the


path length limit. In Windows, the pathnames are limited to 260
characters length. We don’t have that limitation in macOS or
Linux. Python has introduced an option for disabling path length
limit to avoid path length related problems. You can select
disabling the path length limit, which is recommended. It will not
affect any other system settings. Disabling it will resolve potential
path length issues that may arise with projects developed in Linux
or macOS.

Verify Python installation:


After the last screen, your Python setup is completed and you
can verify if it was installed successfully. To verify it, open the
command line on your machine (CMD on Windows, Terminal on
macOS and Linux) and type python --version . You should see
the installed Python version on your machine as below:

Figure 2-6: python --version command on Windows CMD

See where Python is installed:


We have installed Python and we verified the installation by
printing its version on command line. Now let’s see where it is
installed. In other words, we want to see the installation folder or
folders for Python. You might have different Python installations
on the same machine. To see the locations, type where python on
the command line as below:

Figure 2-7: Locations of separate Python installations

As you see in Figure 2-7 there are multiple versions of Python


installed on my machine. We can select any of them while creating
our projects in PyCharm. You will see how in the next sections.
Python Virtual Environments
Virtual Environment is an isolated runtime environment that
allows Python users and applications to install and upgrade Python
packages without interfering with other Python applications
running on the same system.
Python applications will often use packages and modules that
don’t come as part of the standard library. Applications will
sometimes need a specific version of a library, because the
application may require that a particular bug has been fixed or the
application may be written using an obsolete version of the
library’s interface.
This means it may not be possible for one Python installation to
meet the requirements of every application. If application A needs
version 1.0 of a particular module but application B needs version
2.0, then the requirements are in conflict and installing either
version 1.0 or 2.0 will leave one application unable to run.
The solution for this problem is to create a virtual environment,
a self-contained directory tree that contains a Python installation
for a particular version of Python, plus a number of additional
packages.
Different applications can then use different virtual
environments. To resolve the earlier example of conflicting
requirements, application A can have its own virtual environment
with version 1.0 installed while application B has another virtual
environment with version 2.0. If application B requires a library be
upgraded to version 3.0, this will not affect application A’s
environment.[3]
Figure 2-8: Different Virtual Environments on the same OS, completely
isolated from each other

We will use separate virtual environments (venv for short) for


each project we create in this book. That’s the recommended way
for Python development. Don’t worry, you will see how to create
virtual environments in the next sections.
About PyCharm
In this book we will be using PyCharm as our development
environment. PyCharm is a dedicated Python Integrated
Development Environment (IDE) providing a wide range of
essential tools for Python developers, tightly integrated to create a
convenient environment for productive Python, web, and data
science development[4]. PyCharm is a cross-platform IDE that
works on Windows, macOS, and Linux.
PyCharm is available in three editions:
Community (free and open-sourced): for individual Python
development, including code assistance, refactorings, visual
debugging, and version control integration.
Professional (paid): for professional Python, web, and data
science development, including code assistance, refactorings,
visual debugging, version control integration, remote
configurations, deployment, support for popular web frameworks,
such as Django and Flask, database support, scientific tools
(including Jupyter notebook support), big data tools.
Edu (free and open-sourced): for learning programming
languages and related technologies with integrated educational
tools.
We will be using the Community Edition in this book and it is
more than enough for small to medium Python projects. In the next
sections we will install PyCharm and learn its basics.
PyCharm Installation
To install PyCharm, open the official PyCharm web site in your
web browser and navigate to the Downloads page. You can select
your operating system here. We will download the Community
edition which is free.

Figure 2-9: PyCharm download page

It is very straight forward to install PyCharm. Just click Next


and start installation. It is strongly recommended to accept the
default destination folder. Then you can click Next and finalize the
installation.
Figure 2-10: PyCharm setup

When you open PyCharm for the first time, you will see the
default Welcome Screen. Here you have 3 options:
You can create a New Project
You can Open an existing project
You can get an existing project from a Version Control
System (VCS) such as Git.
Figure 2-11: Welcome screen in PyCharm

On the Welcome Screen, you can customize the color theme of


your PyCharm IDE. You can use the black theme for example. The
default black theme is called Darcula in PyCharm. Here is how
you can change your theme:
Figure 2-12: Changing the color theme in PyCharm
First Project in PyCharm
Now that PyCharm is installed on our machine, let’s create our
first project. On the Welcome Screen, click on New Project button
to start the project creation wizard. You see the New Project wizard
in Figure 2-13. Here you select a location and a name for your
project. We name our first project as My_Project which is the last
part in the location string.

Figure 2-13: New Project wizard in PyCharm

The second configuration is about the interpreter.

Python Interpreter:
Python Interpreter is the application that runs your Python
code. More precisely, it is the python.exe file which we installed in
the previous sections. We can either create a new virtual
environment or we can use the base environment with the Base
Interpreter. As stated earlier, working with isolated virtual
environments is the recommended way and that’s what we
generally do in this book. So, we will select the New environment
using Virtualenv option.

Figure 2-14: New virtual environment option

When we select the New environment using Virtualenv


option, PyCharm creates a default location path for our new virtual
environment. Which is a new folder in the project as:
My_Project\venv . PyCharm will create this folder automatically
and it will put the python.exe file and other necessary files into it.
It will use the Base Interpreter to copy the python.exe file. In other
words, PyCharm will copy the python.exe file in the Base
Interpreter and paste it into the venv folder. You can select the
Python version for the Base Interpreter.
The third configuration is about the main.py file. You can tell
PyCharm that you want a default main file in your project. In
general, main.py files are the starting point of Python projects. If
you select this option, PyCharm will create it automatically. If you
do not select it, you can create it later in your project. Let’s select it
and click on the Create button to initialize our first PyCharm
project.
It may take some time, for PyCharm to create the new project
with a new virtual environment. And after it finishes creation, you
will see the main.py file and all the folders in your project.
Figure 2-15: New project in PyCharm

As you see in Figure 2-15, the venv folder is your virtual


environment. And inside venv folder you see python.exe under
the Scripts folder. That’s our Python Interpreter.
You see the main.py file is open on the right hand side of
PyCharm. This is the default file, PyCharm creates for us.
Let’s run our first project in PyCharm. On the top right of the
PyCharm screen you see the Run button with a green play icon.
Click on it, and you will see the output at the bottom of the screen:
Figure 2-16: The run button and the output window in PyCharm

In Figure 2-16 we run our project and it prints “Hi,


PyCharm” in the output window.
If you could run your first project without getting any errors,
this means your Python and PyCharm installations are successful
and you can start development in PyCharm. We will learn
PyCharm basics in the next section.
PyCharm Basics
PyCharm user interface is composed of four main parts: Main
Menu, Project Explorer, Code Editor and Output Window. In
this section, we will see the default PyCharm layout. You can
customize it and change the layout as you want.

Figure 2-17: PyCharm User Interface

Main Menu:
On the top you see the Main Menu, where you have all the
commands related to PyCharm IDE, configuration settings and file
editing options. Here are the items in the main menu and some of
the important functionalities they have:
File : Create a new project, open an existing one, save options
and IDE settings
Edit : Copy, paste, delete functions, find and replace, line
operations
View : Viewing options for tool windows and appearance
Navigate : Backward and forward navigation, navigate in files
Code : Code completion & inspection, folding and formatting
options
Refactor : Refactoring and extracting code fragments
Run : Run and debug with configurations
Tools : Tasks and team configurations
VCS : Version Control System settings like Github and Space
Window : Layout and Tabs configuration
Help : Keymap Reference, Demos and Screencasts, Learn IDE
Features

Project Explorer:
Project Explorer is on the left side and it’s where you manage
project files, folders and virtual environments. For the current
project you can switch between different views as Project, Project
Files and Open Files.

Figure 2-18: Project Explorer options in PyCharm


Code Editor:
On the right hand side we have Code Editor. We use the Editor
to read, write, and explore the source code. You can have multiple
files open in the editor. On the top right corner of the editor
window, you see the quick access buttons like Run, Debug, Run
Configurations, Search and Settings.

Figure 2-19: Quick Access buttons on top of the editor window

Output Window:
On the bottom of the screen we have the Output Window. It
displays the output generated by our application and messages from
the Python Interpreter.
Here are the tabs in the Output Window:
Run: The output when you run the code. You can rerun the
code or edit run configuration

Figure 2-20: Run tab in the output window

TODO: Displays the TODO lines in the code. TODO’s are


special comment lines that starts as ‘# TODO’. In the image below
you see two TODO lines in the code and their listing in the TODO
tab:

Figure 2-21: TODO statements in the code and in TODO tab

Problems: Displays the errors and problems in the code and the
entire project

Figure 2-22: Problems tab

Terminal: An integrated terminal in PyCharm. You do not


need to start a standalone terminal for Python. This terminal works
the same as your operating system terminal.

Figure 2-23: Integrated Terminal in PyCharm

Python Packages: This tab displays all the packages you have
in the current project. You can view package documentation and
add new packages in this tab.

Figure 2-24: Python Packages tab

Python Console: Integrated Python console. Python console is


the text editor where you can run Python code. See an example in
the screenshot below:

Figure 2-25: Python Console


You are now familiar with the PyCharm user interface and I
think it’s enough to start development in PyCharm IDE. If you
want to learn more about PyCharm and how to customize its look
and feel, here is the complete documentation for PyCharm user
interface on its official web site[5].
Running Code in PyCharm
You have three options to run a project in PyCharm:
1- The Run button in the Context Menu by right clicking in the
editor.

Figure 2-26: Run button in the context menu of editor

2- The Run button in quick access menu on the top right of the
editor.
Figure 2-27: Run button in the quick access menu

3- The Run button in the Context Menu by right clicking on the


file name in the Project Explorer.

Figure 2-28: Run button in the context menu of project explorer

And also there is a keyboard shortcut for Run command:


Windows: Ctrl + Shift + F10
macOS: ⌃ ⌥ R

When you press any of these Run buttons or use the keyboard
shortcuts, Python Interpreter compiles and executes the code. Keep
in mind that, the interpreter does compilation and execution line by
line at run time.
In our current project, My_Project , we only have one Python
file which is main.py . In general, Python projects have a starting
point which is called the main file. Here, our main file is the
main.py file which is created by PyCharm when we initialized the
project (Figure 2-13).
You can download the Project in the Github Repository of this
book. Here is the content of the main.py file:

[1]: 1 # This is a sample Python script.


2
# Press Shift+F10 to execute it or replace it with
3
your code.
# Press Double Shift to search everywhere for
4
classes, files, tool windows, actions, and settings.
5
6
7 def print_hi(name):
# Use a breakpoint in the code line below to debug
8
your script.
print(f'Hi, {name}') # Press Ctrl+F8 to toggle
9
the breakpoint.
10
11
# Press the green button in the gutter to run the
12
script.
13 if __name__ == '__main__':
14 print_hi('PyCharm')
15
# See PyCharm help at
16
https://www.jetbrains.com/help/pycharm/
17
18 # TODO - Refactor this code
19
20 # TODO - Define a global variable

When we run the project, Python Interpreter executes the code


in cell 1 starting from the first line.
We have a function definition in line 7, and the function name
is print_hi . This function takes a parameter as name and prints it
in an f-string.
In line 13, we have an if statement as: if __name__ ==
'__main__' . It checks whether the global variable __name__ is
equal to text '__main__' or not. If it is True , then it calls the
print_hi function as: print_hi('PyCharm') .
The __name__ is a special variable which is defined by
PyCharm when we first create the project. You can see it in the
Python Console in the bottom of the screen. You can set a new
value for it if you want by right clicking on it.
Figure 2-29: The special variable __name__ in the Python Console

You don’t have to have a main.py file or a variable called


__name__ . You don’t have to check its value as we did in line 13.
Later on we will change all of these, but for the time being, I
wanted to show you how the things work when PyCharm initializes
a project and how it runs the main file.
Interpreter Configuration
When you run the project, PyCharm looks for the Python
Interpreter and tells it that it should run the code. So the interpreter
configuration is crucial. If PyCharm cannot find it you will get an
Interpreter Error. As we stated earlier the interpreter is simply
the python.exe file and it resides in a virtual environment. Now
we will prove this.
We have a project named My_Project in this chapter. And it
has its own virtual environment in the venv folder. Remember
Figure 2-15, where you can see the python.exe file under the
path: \My_Project\venv\Scripts . You can navigate to this path
from PyCharm, by right clicking it and Open In > Explorer.
Figure 2-30: Open PyCharm files and folders in file explorer

All of your Python packages, libraries and Python Interpreter


reside in venv folder which is your virtual environment. Now that
we know the Python Interpreter is in the venv folder let’s delete it
and see what happens. To do this, navigate to the top level folder of
your project, namely the project folder and delete the venv folder.
You may need to close the PyCharm first, because the folder will
be in use when you try to delete it.

Figure 2-31: Delete venv folder in the project


Now reopen PyCharm after you deleted the venv folder. You
will see that, it will display an error as Invalid Python
Interpreter selected for the project. This means that you don’t
have a Python Interpreter for this project.

Figure 2-32: Interpreter Error after deleting venv folder

Let’s fix this error and configure the interpreter. There are
multiple ways to access the interpreter configuration screen. The
first way is to click on the Configure Python Interpreter button
which is at the right of the error text. The second way is to click on
the file name (which is main here) at the quick access menu and
then select Edit Configurations. See the image below:

Figure 2-33: Edit Configurations button in the quick access menu

And the third way is under File > Settings:


Figure 2-34: File > Settings

Let’s move on with File > Settings way. When you click on
the Settings button under the File menu it will open the settings
window for PyCharm. This is the main screen where we do almost
any customization in PyCharm.

Figure 2-35: Settings Window and Python Interpreter configuration

On the Settings Window, under the Project section, you see


Python Interpreter. And you will see the Invalid text in the Python
Interpreter field on the right (Figure 2-35). Now we will configure
a new interpreter.
To configure a new interpreter click on the gear (settings) icon
on the right and then click on Add… button (Figure 2-36).

Figure 2-36: Add new Python Interpreter button

PyCharm will open the Add Python Interpreter window as


below:

Figure 2-37: Add Python Interpreter window

In Figure 2-37, the Virtual Environment tab is selected. Here


you can either create a new virtual environment or use an existing
one. We will create a new virtual environment. As you see, the
default folder name is venv . When we click the OK button,
PyCharm will create this folder and a new virtual environment in it.
We will do it in a minute but before that, let me show you the
System Interpreter on this screen.

System Interpreter:
If you do not want to create a new virtual environment and a
new interpreter, you can use the System Interpreter. It’s the base
Interpreter you have when you install Python. To see it, click on
the System Interpreter tab on the left.

Figure 2-38: System Interpreter

When you click on the System Interpreter tab, you will see all
the Python Interpreters on your machine (Figure 2-38). If you click
OK, your current project will be using the Base Interpreter instead
of creating a new virtual environment and a new interpreter.
As we stated earlier we will create a new virtual environment
for this project, so click on the Virtual Environment tab again
and then click the OK button (Figure 2-37). PyCharm creates a
new virtual environment for us in the venv folder. Now you can
see that the Interpreter Error has been resolved and the new venv
folder created in the project. See the final figure below:
Figure 2-39: New virtual environment (venv folder) in the project
Debugging in PyCharm
In computer programming, Debugging is the process of
detecting and removing of existing and potential errors. These
errors are also called as ‘bugs’. The bugs in a program can cause it
to behave unexpectedly or crash. In this section we will see how to
do debugging in PyCharm.
Debugging is done via breakpoints. Breakpoints are the special
places in the code where you intentionally stop (pause) the
execution of the program. This is a temporary stop and we do this
to see what’s going on in our program.
The breakpoints are only operational in debug mode. They do
not work in normal run mode. In other words, to be able to debug
your code at breakpoints you need to run the application in debug
mode. We will see it in a minute.
To place a breakpoint in PyCharm, you simply click on the
gutter once. The gutter is the column where you see the line
numbers. In the image below we put a breakpoint on line 9:

Figure 2-40: A breakpoint in the gutter


We have a breakpoint on line 9, which means PyCharm will
pause execution on this line. And we will have look in our program
state.
Now let’s run the project in debug mode. In Figure 2-40, you
can see the debug run button with a bug icon on the quick access
bar. Let’s click on this button and run our application in debug
mode.

Figure 2-41: PyCharm in Debug Mode

When you run the application in debug mode, you will see a
new window in the bottom, which is the Debug screen (Figure 2-
41). There are two main tabs on the Debug window, Debugger and
Console. Under the Debugger tab, you can see two parts which are
Frames and Variables. Frames tab displays the execution order of
your program and the Variables part is where you manage your
variables. For example, we only have one variable in the print_hi
function, which is name . The value for this variable is
‘PyCharm’ .
Resume Program:
You can continue the execution with the Resume Program
button (Figure 2-42). PyCharm will continue to execute and it will
finish execution if it does not encounter another breakpoint. If it
encounters another breakpoint it will pause again and wait.
Stop:
If you want to stop debugging and exit the Debugger you can
use the Stop button (Figure 2-42). It will terminate the current
debug session and set the program to its initial state.

Figure 2-42: Resume Program and Stop buttons in Debugger

Now let’s see debugging in more detail. We will first stop the
current debug session. Click on the stop button in the debugger
window. Then create a new Python file (.py) in our project and
name it as debug_example.py . To create a new file, in the Project
Explorer pane, right click on the project name and select New >
File as in Figure 2-43. And name the file as debug_example.py .
Figure 2-43: Create a new Python file in project

We will define a function in this new file. Then we will call this
function with some arguments. Here is the complete code for the
debug_example.py file:

[2]: 1 # define a function to add numbers


2 def summation(x_1, x_2):
3 sum_of_nums = x_1 + x_2
4 return sum_of_nums
5
6 # define two variables
7 a = 2
8 b = 5
9
10 # call the function
11 summation(a, b)

We will set two breakpoints in this file, on lines 3 and 11. See
the image below:
Figure 2-44: Breakpoints in debug_example.py file

Now our debug_example.py file is ready with breakpoints.


Before running it in debug mode, there is a configuration we
should change. The run configuration. Currently, it is set to run the
main.py . You can see it in Figure 2-44. So if you click Run or
Debug buttons it will run the main.py file, not our new file. To
change this, right click in the code editor of debug_example.py
file and select Debug ‘debug_example’.
Figure 2-45: Run debug_example.py file

When you click on Debug ‘debug_example’, PyCharm runs


the debug_example.py file in debug mode. As you see in Figure
2-46, the run configuration is debug_example now. PyCharm
pauses the execution on line 11 and launch the Debugger at the
bottom. Line 11 is where we call the summation function.
Figure 2-46: debug_example.py file is in debug mode

Now the code execution waits on line 11. In Figure 2-46, you
can see that we have to variables a and b and their values. If you
click on the Resume Program button, which is the play like green

icon, , the code execution will continue until it stops at line 3.


Why? Because we have another breakpoint on line 3. See the
image below:
Figure 2-47: Second breakpoint is in the summation function

Now in the Debugger we see the summation function on the


left the frame and on the right x_1 and x_2 are the variables in
this frame.
There is a set of important buttons on the top bar of the
Debugger window. These are the stepping toolbar buttons for the
debugger. Here they are:

Figure 2-48: Stepping toolbar buttons in Debugger

Here are the functionalities for the most common stepping


buttons:
Step Over: Executes the current line.
Step Into: Steps into the method called at the
current execution point.
Step Out: Steps out of the current method, to
the line executed right after it.
Evaluate Expression: Evaluates Python
expressions.

Now let’s evaluate an expression. Remember our debugger is


waiting for execution on line 3. And we have an addition operation
in this line as: sum_of_nums = x_1 + x_2. Before running this
line, let’s evaluate it. Click on the Evaluate Expression button
(calculator icon) in the stepping toolbar. It will pop up the
evaluation window as follows:

Figure 2-49: Evaluate window

The Evaluate window is very useful and you will be using it


most of the time in your projects. You can evaluate an expression,
you can do a function call, and you can define variables on this
window. It doesn’t affect your project execution. It’s like a separate
Python environment which can use the current project. In Figure 2-
49 we evaluate the expression of x_1 + x_2 and it returns 7 .
Let’s click the Step Over (F8) button to run the current line,
which is line 3. PyCharm runs the code in line 3 and it waits on line
4. Because Step Over button only runs one line of code. See the
image below:

Figure 2-50: The new line of execution is 4

As you see in Figure 2-50, after we click the Step Over button,
PyCharm executes line 3 and it creates a new variable which is
sum_of_nums . You can see it in the Variables tab in Debugger.
The current line is line 4 now. If we click on Step Over button one
more time, PyCharm will finish the execution of the summation
function and it will return to the function call in line 11. And if you
click it one more time, PyCharm will end the running because there
is no more lines to execute.
As a final example, you can remove the breakpoint on line 3
and try the Step Into button to be able to pause inside the
summation function. Here is the image for it:

Figure 2-51: Step Into the function

I think that’s enough for debugging in PyCharm. You learned


the basics of debugging, how to set breakpoints and how to run and
evaluate the code line by line.
We are ready to start actual Python development now. In the
next chapter you will learn Exception Handling in Python.
3. Exception Handling
In this chapter, you will learn one of the most important
concepts in programming which is Exception Handling.
Exception Handling is the key if you want to develop solid and
unbreakable programs. Exception Handling is the process of
responding to the occurrence of exceptions during the execution of
the program.

We will create a new PyCharm project for this chapter. The


project name is Exception_Handling. You are recommended to
create a new project with a new virtual environment. You can also
download the project from the Github Repository of the book if
you want. Here is the project that we will develop in this chapter:
Figure 3-1: Exception_Handling project for this chapter
Difference between Exception and Syntax Error
A Python program stops execution when it encounters an error.
The process of managing these situations is called Exception
Handling.
There are two types of errors in Python:
Syntax Error (Design-Time error)
Exception (Run-Time error)

Syntax Error (Design-Time error):


If the code you write doesn't comply with Python syntax, the
Python Parser will raise 'Syntax Error'. In many cases the error
type will be SyntaxError which tells you that the code you write
is not suitable for Python.

To start with let’s create a Python file in the project. The file
name will be _1_exception_vs_syntax_error.py . Make sure you
use an underscore before the number 1 as _1 . In general it is a
good practice not to start with a number for file names.
Now that we create the file, let’s see examples of Syntax Errors.
In the first example we will add an extra parenthesis at the end of
print() function.

[1]: 1 # Ex:
2 # SyntaxError
3 print('Python Parser error'))

[1]: SyntaxError: unmatched ')'

If you run the code in cell 1, you will get a SyntaxError that
tells you the parenthesis at the end is not matched. Here is the
image for the error in the output window in PyCharm:

Figure 3-2: SyntaxError in PyCharm

Let’s do another example. This time let’s try to define a string


variable intentionally in wrong way. We will start with double
quotes but end with single ones.

[2]: 1 # Ex:
2 # SyntaxError: EOL while scanning string literal
3 a = "12'
4 print(a)

[2]: SyntaxError: EOL while scanning string literal

As you see in the output of cell 2, we get a SyntaxError which


tells us that string is not defined properly. EOL means End Of
Line and it states that the end of line is not as expected. It should
be a double quote.
Here is another example. Let’s see an indentation error this
time.

[3]: 1 # Ex:
2 # IndentationError: expected an indented block
3 def myFunc():
4 # function scope
5 print('A')

[3]: IndentationError: expected an indented block

As you see in cell 3, we start the function scope of myFunc in


line 4 but there is no code lines in it. The print(‘A’) statement in
line 5, is not in the function scope. Why? Because it should have an
indent to be in the function scope. Since it is the first code line after
the function definition, Python expects it to be indented. But it’s
not. That’s why, Python Parser raises an IndentationError .
Syntax Errors are easy to see and fix. Because as soon as you
write the code or you run it for the first time, you see them. You are
notified by PyCharm. And you can fix it very easily. But this is not
the case for Exceptions.

Exception:
Let's assume, your code is syntactically correct. So it will start
execution. If it encounters an unexpected situation (error) during
the execution (run-time), Python Interpreter will raise an error. This
is error is called Exception.
In the first example, let’s see a very common error when you
have to deal with mathematical operations in your code. It is the
error that occurs when you try to divide a number by zero.

[4]: 1 # Ex:
2 # ZeroDivisionError: division by zero
3 a = 7 / 0

[4]: ZeroDivisionError: division by zero

In cell 4, we try to divide 7 by 0 and assign the result to the


variable a . This expression is syntactically correct. For Python
Parser there is nothing wrong in it. But it’s not a valid
mathematical operation. So at run-time, when Python Interpreter
tries to calculate the result, it will raise an exception. The exception
type is ZeroDivisionError .
Let’s see another exception. This time let’s try to use a variable
which is not defined yet.

[5]: 1 # Ex:
2 # NameError: name 't' is not defined
3 print(t)

[5]: NameError: name 't' is not defined

In cell 5, we try to print a variable named t . But there is no


variable in this name. So Python Interpreter raises a NameError
exception. Be careful that, this expression is syntactically correct.
In the last example let’s try to add a number to a string. Here is
the result:

[6]: 1 # Ex:
2 a = 12
3 b = 'B'
# TypeError: unsupported operand type(s) for +: 'int'
4
and 'str'
5 print(a + b)

TypeError: unsupported operand type(s) for +: 'int'


[6]:
and 'str'

As you see in cell 6, line 5, we try to add an integer to a string.


This is not allowed in Python. For addition operation both of the
operands should be of the same type (either integer or string).
That’s why Python Interpreter raises a TypeError .
Now that we know what exceptions are, let’s learn how we can
handle them in the succeeding sections.
Raise, Assert
No matter how hard you try to make your code free of bugs,
unfortunately at some points there might be errors. What will you
do in such cases? You have to handle these errors properly to
prevent your program from crashing. You have to raise appropriate
exceptions and decide what to do with these exceptions.
We will create a new Python file for this section. The file name
is _2_raise_exceptions.py . To run this file, right click on it in the
code editor and select Run ‘_2_raise_exceptions.py’. You
should see the file name in the run configuration as below:

Figure 3-3: Name of the running file in the run configuration

raise :
Appropriate Exceptions will help you to debug your code more
easily. Moreover it will let other applications, who call your code,
to understand what happened. To raise an exception in Python we
use the raise keyword.
Let’s see how we use the raise keyword with an example. We
will ask for an integer from the user. If the user does not enter an
integer, we will raise an Exception.
Before writing the complete code for this example, let’s see
what happens if we do not care for exceptions in our code:

[7]: 1 # without exception handling


2 def raise_exception():
3 # ask for user input
4 user_input = input('Please enter an integer: ')
5
6 # if we do not handle the error -> crash
# ValueError: invalid literal for int() with base 10:
7
'asdf'
8 num = int(user_input)
9 print(num)

In cell 7, we define a function named raise_exception . We ask


for the user input in line 4. And in line 8, we try to convert this
input to an integer as: num = int(user_input). But there is a
problem here. What if the user enters a text which cannot be
converted to integer? Let’s see this problem by calling
raise_exception function and passing some arbitrary text to it:

[8]: 1 # call the function


2 # and pass 'asdf' to it
3 raise_exception()

[8]: Please enter an integer: asdf


ValueError: invalid literal for int() with base 10:
'asdf'

As you see in cell 8, we pass a string of ‘asdf ’ to the function


and the program crashes. It raises a ValueError telling us that it
couldn’t convert ‘asdf ’ to an integer.
That’s a very typical case, where the programmer does not have
a proper exception handling mechanism. As a programmer you
should be aware of the possible places where an exception might
occur in your code. And you have to handle these cases. We will
not do an actual exception handling in the next section, but we will
do it later. For the time being, let’s raise an exception to show that
we are aware of the problem.
[9]: 1 def raise_exception():
2 # ask for user input
3 user_input = input('Please enter an integer: ')
4
5 # if we do not handle the error -> crash
# ValueError: invalid literal for int() with base
6
10: 'asdf'
7 if not user_input .isdigit():
8 raise Exception('Not a number...')
9
10 # user input is an integer
11 num = int(user_input)
12 print(num)

Important note about isdigit():


isdigit() method returns True if all characters in the string are
digits and there is at least one character, False otherwise. That’s
why it works only for positive, unsigned integers. It doesn’t work
for negative numbers.

In cell 9, we redefine the raise_exception function. It starts by


asking for the user input as we did before.
The difference is in line 7. It checks the condition where the
user_input is not an integer as: if not user_input.isdigit(). If
that condition is True , then this means that the input is not an
integer. And it raises an Exception telling that it is not a number.
The syntax is: raise Exception('Not a number...'). We put the
exception type after the raise statement which simply the
Exception class here. Exception is the general type for exceptions
in Python. And inside the parentheses of Exception constructor
we put the error description as: Exception('Not a number...').
The raise statement terminates the current execution and exits
the function. If you do not handle it properly your program will
crush. Don’t worry we will prevent it from crashing later on.
In line 11, we convert the u s e r _i n p u t to an integer. At this
stage, we are perfectly sure that it is an integer now. Why? Because
if it wasn’t, then the if block in line 7 would execute and the raise
statement would finalize the execution. If the execution comes to
the line 11, then this means user_input is an integer. So we can
convert it to int now.
In the previous example we used the generic exception class
which is simply Exception . We can do better. In cell 8, we saw
that, Python raises ValueError when the int(user_input)
function cannot convert the given argument to an integer. We can
use the same type in our code. Let’s define a new function and
raise the ValueError instead of generic Exception .

[10]: 1 # raise ValueError exception


2 def raise_defined_exception():
3 user_input = input('Please enter an integer: ')
4
5 # if we do not handle the error -> crash
# ValueError: invalid literal for int() with base
6
10: 'asdf'
7 if not user_input .isdigit():
8 raise ValueError('Not a number...')
9
10 # user input is an integer
11 num = int(user_input)
12 print(num)

In cell 10, line 8, we raise the ValueError if the user input is


not an integer. Let’s call this function and pass a string which
cannot be converted to an int.

[11]: 1 # call the function


2 # and pass 'asdf' to it
3 raise_defined_exception()

[11]: Please enter an integer: asdf


ValueError: Not a number…

As you see in cell 11, we raise our own ValueError exception


with our custom text in it. And that’s how we raise exceptions in
Python.

assert :
The if statement in the raise_defined_exception function (in
cell 10) was actually an ‘assertion’ operation. That means, if the
code has not been asserted, it will not move on. In other words, the
assertion in the if statement stops execution if the user input is not
an integer.
In Python, for checking assertions we have a special keyword as
assert . In the raise_defined_exception function, we can use
assert statement instead of the if statement.
The syntax is: assert <condition_to_check>
If 'condition_to_check' is not True , then Python will raise an
exception. We mainly use assertion statement for debugging
purposes.
Let’s see how we use the assert keyword with an example. We
will do the same integer check with the help of the assert
statement.

[12]: 1 def assert_user_input():


2 user_input = input('Please enter an integer: ')
3
4 # assert the input being integer
assert int(user_input), ValueError('Not asserted
5
as integer...')
6
7 # user input is an integer
8 num = int(user_input)
9 print(num)

In cell 12, line 5, we see an assert statement as: assert


int(user_input), ValueError('Not asserted as integer...'). Here,
int(user_input) is the condition to check. If it’s True then
nothing will happen. Python will continue to execute the code and
it will move to the line 8. But if the condition is False , then Python
will raise a ValueError exception as: ValueError('Not asserted
as integer...').
Let’s do another example on the assert statement. We want to
define a function for doing division on integers. When we say
‘division’, the first thing to consider should be 'division by zero'
error. We have to do an assertion to avoid this error. Let’s do it:

[13]: 1 def division(n1, n2):


2 # assert the divisor not being zero
3 # AssertionError -> exception
assert n2 != 0, 'You cannot divide by zero. -
4
Custom'
5
6 print(n1 / n2)

In the division function in cell 13, we have two parameters,


n1 and n2 . In line 4, we check whether the second parameter is
zero or not. The condition is: n2 != 0. If it’s not zero, which means
the assertion is successful, then the code will continue to execute. If
it is zero, then the assertion will fail and Python will raise
AssertionError . AssertionError is the default exception type for
the assert statement.
Let’s call the function with valid numbers for division:

[14]: 1 # call of function with valid numbers


2 division(10, 2)

[14]: 5.0

Now, let’s call the function with zero as the second parameter
and see what happens:

[15]: 1 # call with zero


2 division(45, 0)

[15]: AssertionError: You cannot divide by zero. - Custom

As you see we get an AssertionError when we call the


function with zero. And the exception text is what we enter in cell
13, which is 'You cannot divide by zero. - Custom'.
try-except
In the previous section we learned how to raise exceptions in
case of an unwanted or unexpected condition. Raising exceptions
doesn’t mean too much if you do not handle them. In this section
you will learn the basic structure for handling exceptions in Python.
The try-except block in Python is used to catch and handle
exceptions.
We will create a new Python file for this section. The file name
is _3_try_except.py .

try:
....
............
......error......
........ will not be executed
..... will not be executed
....... will not be executed
except:
........
........

In the pseudocode above, you see the basic structure of a try-


except block. Here is the execution flow:
Python runs the code in the try block,
If it encounters an error (exception) the code execution will
jump to the except block
Any line in the try block which is under the error line will
not be executed
If no exception occurs, the except clause is skipped and
execution of the try statement is finished.
The except block is where you catch the exceptions. You take
the necessary actions in case of an exception in the except block.
Let’s see an example. We want to define a function which
calculates and returns the square of a given number. First we will
define it with no exception handling in it. Then we will redefine it
with a try-except block.

[16]: 1 # Ex:
2 # with no exception handling
3 def get_squares():
4 user_input = input('Enter a number: ')
5 num = int(user_input)
6 print(num**2)

In cell 16, we define the get_squares function. It asks for an


input from the user. And it assumes that the input will be a number.
Then in line 5, it converts the user_input to an int and assign it to
a variable called num . And finally, it prints the square of this num
variable.
This function will run perfectly if the user enters a number all
the time. But it will crash when the user input is not a numeric
value. Let’s test it by calling it passing some arbitrary text which is
not numeric:

[17]: 1 # call the function and pass some text


2 get_squares()

[17]: Enter a number: asdf


ValueError: invalid literal for int() with base 10:
'asdf'

As you see in cell 17, we pass some arbitrary text as ‘asdf ’ and
our program crashes. We get an exception of type ValueError and
Python Interpreter terminates the program execution. The reason is
the line 5 in the function. It is where we try to convert the user
input to an integer.
Now let’s redefine this function with a try-except block. We
will do our operations in the try block. The try block is like a
safe place to run your code. If any exception occurs we know that
the execution will jump to the except block.

[18]: 1 # with exception handling


2 def get_squares_try():
3 try:
4 user_input = input('Enter a number: ')
5 num = int(user_input)
6 print(num**2)
7 except:
8 print('Not a number...')

In cell 18, we write almost all the code in the try block. That’s
very common in programming. The try block gives you the
opportunity to control the execution flow in case of any exceptions.
And in line 7, we have the except block. When an exception
occurs in the try block, the except block will be executed and it
will print a text of 'Not a number...'.
Let’s call this function and pass the same text of ‘asdf ’ :

[19]: 1 # call the function and pass some text


2 get_squares_try()

[19]: Enter a number: asdf


Not a number…

As you see in cell 19, we call the function and pass a text which
is not an integer and it printed the text of 'Not a number...'. Our
program did not crash and we don’t see any exceptions in the
output. We handled a possible exception in our function.
Let’s add some functionality to the except block in the
get_squares_try function. After printing 'Not a number...' text,
let’s ask for a new input. We will do it with the help of Recursion.
Recursion is the case when the function calls itself. Our function
will call itself to ask for a new input if the user enters a text which
is not numeric.

[20]: 1 # call the function -> recursion


2 def get_squares_try_recursion():
3 try:
4 user_input = input('Enter a number: ')
5 num = int(user_input)
6 print(num**2)
7 except:
8 print('Not a number...')
9 get_squares_try_recursion()

In cell 20, line 9, our get_squares_try_recursion() function


calls itself in the except block. This means that it will keep asking
for a number until the user enters a numeric value. Let’s call it and
see:

[21]: 1 # call the function and pass some text


2 get_squares_try_recursion()

[21]: Enter a number: asdf


Not a number...
Enter a number: xyz
Not a number...
Enter a number: 20
400

Let’s do another example now. This time, let’s try to open a file
with Python. We will define a function which will take the path of
the file as the parameter and open it. Then it will read the content
of the file line by line and print the lines. We will define this
function with no exception handling first, then we will redefine it
properly.
We need a file in our project to be able to open it. Let’s create a
text file. Right click on the project name and then New > File. The
file name is series.txt . It will be a plain text file and contain some
TV series. Here is the content of this file:

Queen's Gambit
Fargo
Dark
True Detective
Dogs of Berlin
The Crown

Now that we a file to open and read, let’s define our function:

[22]: 1 # Ex:
2 # Open a file.
3 # no exception handling
4 def open_file(path):
5 # open()
6 file = open(path)
7
8 # loop over file line by line
9 for line in file:
10 print(line.split())

In cell 22, we define the open_file function. It has no


exception handling in it. In line 6, we open the file in that path as:
file = open(path). What if, there is no file exist in that path? We
will have an exception in this case and it may crash the program if
we do not handle it. Let’s see what happens if we pass a file path
which doesn’t exist.

File paths in the same directory:


If the file to open is in the same directory of our Python code,
we don’t have to pass the full path. It is enough to pass the file
name.

[23]: 1 # call the function with wrong file path


2 path = 'serieeees.txt'
3 open_file(path)

FileNotFoundError: [Errno 2] No such file or


[23]:
directory: 'serieeees.txt'

In cell 23, we call the function with a path of 'serieeees.txt'


and we know that there is no file with that name. That’s why get an
exception of type FileNotFoundError . The error description tell
us that No such file or directory: 'serieeees.txt'.
Now let’s add a try-except block in this function to make it
robust against exceptions. We know that, if there is no file in the
given path, Python will raise an exception. We will handle it now.

[24]: 1 # handle the case where file not exist


2 def open_file_try(path):
3 try:
4 # open()
5 file = open(path)
6
7 # loop over file line by line
8 for line in file:
9 print(line.split())
10 except:
11 print('No such file in the path:', path)

In cell 24, we write all the code in the try block. If anything
unexpected occurs here we know that the code in the except block
will be executed. And it will print a text stating no such file in this
path. Let’s call it and see:

[25]: 1 # call the function with wrong file path


2 path = 'serieeees.txt'
3 open_file_try(path)

[25]: No such file in the path: serieeees.txt

As you see in cell 25, we handled the exception that will occur
when there is no file in the given path.

Multiple Except Blocks:


We know that, an except block is where we catch exceptions
and take necessary actions. What if we need different actions based
on the types of exceptions. We can do this by using multiple
except blocks. Here is the syntax for multiple except statements:

try:
.....
.....
error
.....
except Exception_Type_1:
...actions for type 1...
.....
except Exception_Type_2:
...actions for type 2...
.....

Let’s see them with an example. In this example, we will define


a function named divide_number . It will ask for two numbers:
dividend and divisor . And it will return the result of the division
operator.
What are the possible exceptions here? The first one is about
the user inputs. User inputs may not be numeric values. The second
exception is about having zero for the divisor. We cannot divide a
number by zero. So we have to be careful about the value of the
divisor.
As you see there are two possible exceptions in this function.
Let’s define two separate except blocks for each type.

[26]: 1 # Ex: user input and zero division


2 def divide_number():
3 try:
4 # ValueError
dividend = int(input('Please enter a number to
5
divide: '))
6 divisor = int(input('Please enter the divisor: '))
7
8 # ZeroDivisionError
9 result = dividend / divisor
10 print(result)
11
12 except ValueError:
13 print('The inputs are not numeric...')
14
15 except ZeroDivisionError:
16 print('Second number cannot be ZERO....')

In cell 26, we have two separate except blocks:


The first one is for the ValueError . This is where we handle
the exception which arises when the user input is not numeric. We
have integer conversion in lines 5 and 6. And if the user input is not
numeric, these lines will raise ValueError . And in the respective
except block we print as 'The inputs are not numeric...'. Let’s
call the function and pass some text which is not numeric:

[27]: 1 # call the function


2 divide_number()

[27]: Please enter a number to divide: 45


Please enter the divisor: abc
The inputs are not numeric...

The second one is for ZeroDivisionError . In line 9, we have a


division operation. This line will raise an exception when the
divisor is zero. And we catch this exception in line 15. It will print
as 'Second number cannot be ZERO....'. Let’s call the function
and pass zero for the divisor:

[28]: 1 # call the function


2 divide_number()

[28]: Please enter a number to divide: 45


Please enter the divisor: 0
Second number cannot be ZERO....

As you see in cells 27 and 28 we have different outputs in case


of different exceptions.
else
The try-except statement has an optional else clause, which,
when present, must follow all except clauses. It is useful for code
that must be executed if the try clause does not raise an exception.
In other words, if the try block runs successfully (with no
exceptions) then the else block will be executed. Here is the
syntax for it:

try:
......
......
......
except Ex1:
......
......
except Ex2:
.......
else:
.......
no-errors

Python Interpreter runs the code in try block. If there is an


error, the execution goes into the related except block. If there are
no errors, then it will move on to else block.
We will create a new Python file for this section. The file name
is _4_else.py .
Let’s see an example for try-except-else structure. We will
define a function named open_file . It will take the file path as the
parameter. The function will try to open the file in the try block. If
it encounters an error, it will notify the user that there is no such
file. If it opens the file successfully then it will read and print the
file content.

[29]: 1 # Ex: open and read the file


2 def open_file(path):
3 try:
4 file = open(path, encoding='utf-8')
5 except FileNotFoundError:
6 print('No file in path:', path)
7 else:
8 print(file.read())

In cell 29, you see the function definition. It opens the file in the
try block (line 4). Don’t worry you will learn file operations in
detail in a Chapter 6.
We already know that if the file doesn’t exist the open()
function will raise a FileNotFoundError exception. That’s why
we put the exception type after the except clause as: except
FileNotFoundError . And inside the except block we print a text
telling that there is no file with this path.
If the open() function can open the file successfully, then it
will assign the resulting object to the file variable. Since the try
block runs without any errors, the code execution will skip the
except block and it will come to the else block. Here in the else
block, we read the file content as file.read() and we print the
result as: print(file.read()) .
Before calling the open_file() function, let’s create a new text
file to read. Its name will be example.txt and here is the content
for this new file:

In Python try-except-else structure:

try:
<---- code ---->
except:
<---- error ---->
else:
<---- no errors ---->

Now let’s call our function with a wrong path first:

[30]: 1 # call function with a wrong path


2 path = 'exampleeeeee.txt'
3 open_file(path)

[30]: No file in path: exampleeeeee.txt

In cell 30, we call the open_file() function with a path that


doesn’t exist. Python Interpreter raises the FileNotFoundError
exception and we catch this exception in the except block. It prints
as No file in path: exampleeeeee.txt.
Now let’s call the same function with the correct file path:

[31]: 1 # call function with correct path


2 path = 'example.txt'
3 open_file(path)

[31]: In Python try-except-else structure:

try:
<---- code ---->
except:
<---- error ---->
else:
<---- no errors ---->
In cell 31, we call our function with the correct path. Since
example.txt file exists in the current directory, the open()
function in the try block runs successfully. Since there are no
errors in the try block, the else block is executed and it prints the
file content.

Get the exception object:


We can get the exception object in the except statements. Here
is the syntax for it: except ExceptionType as <name>. The
<name> variable which we define after the as keyword is the
exception object.
Let’s redefine our open_file function and print the standard
exception description in the except block:

[32]: 1 # get the exception object


2 def open_file_as_exc(path):
3 try:
4 file = open(path, encoding='utf-8')
5 except FileNotFoundError as ex:
6 print(ex)
7 else:
8 print(file.read())

In cell 32, line 5, we get the exception object of type


FileNotFoundError and name it as ex . And in line 6, we print
this object. The print() function on the exception objects prints
their standard error descriptions.
Let’s call the function with a wrong file path:

[33]: 1 # call function with a wrong path


2 path = 'exampleeee.txt'
3 open_file_as_exc(path)

[33]: [Errno 2] No such file or directory: 'exampleeee.txt'

In cell 33, we call the function with a wrong file path and it
prints the exception description as [Errno 2] No such file or
directory: 'exampleeee.txt'.

pass :
The pass statement is a null operation. When it is executed,
nothing happens. It is useful as a placeholder when a statement is
required syntactically, but no code needs to be executed. In loops,
function definitions, class definitions, or in if statements empty
code is not allowed. Which means, in such places, you must have at
least one line of code which is not a comment line. So we use the
pass statement in these cases.
The difference between a comment and a pass statement is
that, pass is not ignored by the Python Interpreter while the
comments are.
In the previous example, let’s say we don’t want to take any
actions if the file does not exist. We simply want to pass the code
further. We can use the pass statement here:

[34]: 1 # Ex: pass


2 def open_file_as_pass(path):
3 try:
4 file = open(path, encoding='utf-8')
5 except FileNotFoundError as ex:
6 # Do nothing if the file doesn't exist
7 pass
8 else:
9 print(file.read())
10 # code after try-except-else block
11 print('Code line after try-except-else')

In cell 34, line 7, we have the pass clause in the except block.
Which means, we will do nothing if the file does not exist with the
given path. We simply pass the execution to the next line which is
line 11. It is the first line after the try-except-else block.
Let’s call the function with a path that does not include a file in
it:

[35]: 1 # call function with a wrong path


2 path = 'exampleee.txt'
3 open_file_as_pass(path)

[35]: Code line after try-except-else

In cell 35, we call the function with a wrong path, which means
the except block is executed. Since we have the pass keyword in
the except block in line 7, the code execution moves to the line 11
and it prints as 'Code line after try-except-else' .
finally
The try statement has another optional clause which is called
finally . In Python, the finally keyword is intended to define
clean-up actions that must be executed under all circumstances.
An example clean-up action might be to close an open file or to
release a resource before finalizing the try block.
If a finally clause is present in the try block, it will execute as
the last task before the try statement completes. The finally
clause runs whether or not the try statement produces an
exception.
Here is the complete syntax for try-except-else-finally
structure:

try:
......
......
......
except Ex1:
error
......
......
except Ex2:
error
.......
else:
.......
no-errors
finally:
whether an error or not
.......
We will create a new Python file for this section. The file name
is _5_finally.py .
Now that we know the complete structure of a try block in
Python, let’s do some examples. In the first example we will revisit
the division function we defined earlier. It takes two parameters,
x and y , and it prints the result of the division operation.

[36]: 1 # Ex:
2 def division(x, y):
3 try:
4 result = x / y
5 except ZeroDivisionError as e:
6 print(e)
7 else:
8 print('Result:', result)
9 finally:
10 print(‘Try block is finished...')

In cell 36, you see the try-except-else-finally structure. In the


try block, we try to divide x by y and assign the result to the
variable called result . If there occurs a ZeroDivisionError
exception here, we will catch it in the except block and print the
exception description. If no errors occur, then the else block will
execute and it will print the result variable. In any case, whether
there is an exception or not, the finally block will execute and it
will print as ‘Try block is finished...'.
Let’s call the function in both cases and see it in action:

[37]: 1 # call with zero


2 division(12, 0)

[37]: division by zero


Try block is finished...

As you see in cell 37, we call the function with zero as the
divisor. It causes a ZeroDivisionError in the try block and the
except block executes. The except block prints the text of
“ division by zero” which is the standard description for
ZeroDivisionError in Python. But we see one more line in the
output which is “ Try block is finished...”. This text is printed in
the finally block in line 10.
Now, let’s call our function with valid numbers and see what
happens:

[38]: 1 # call with valid numbers


2 division(12, 4)

[38]: Result: 3.0


Try block is finished...

In cell 38, we call the function as division(12, 4). These two


numbers does not cause any exceptions in the try block. So the
else block is executed and it prints the result as: Result: 3.0. After
that, the finally block is executed and it prints as: ‘Try block is
finished...' .
As you see in both cases, the finally block is executed
regardless of exceptions.
Let’s do another example. This time we will use nested try
blocks. In the finally block of the first one we will place the
second one. Let’s define the function first:

[39]: 1 # Ex:
2 # close the file in finally
3 def open_file(path):
4 try:
5 file = open(path, encoding='utf-8')
6 except Exception as ext:
7 print(ext)
8 else:
9 print(file.read())
10 finally:
11 try:
12 file.close()
13 print('Closing the file.')
14 except:
15 pass

The aim of the function is simple, we want to open a file. But


we need to handle all possible exceptions which might occur, when
we try to open a file.
In line 5, inside the try block, we use the built-in open()
function to open the file in the given path . The code line is: file =
open(path, encoding='utf-8') . And we assign the resulting file
object to a local variable called file . If there exist an error here,
this file variable will not be defined. So we will not be able to use
it later on.
In line 6, we have the except block. Instead of writing a
specific type of exception like FileNotFoundError we put the
general Exception class in the except block. We will be able to
catch all kinds of exceptions by doing so.
In line 8, we have the else block. If the open() function runs
successfully, then this means we have an object called file . And its
content will be read and printed here as: print(file.read()) .
In line 10, we have the finally block. We know that code
execution will end up in the finally block in any case. We intend
to close the file in line 12 as: file.close() . But there is a problem
here, we don’t know whether the open() function could open the
file successfully or not. If it is successful then we have a variable as
file , but if it is not, then there is no such variable. And if the file
variable is not defined yet, the file.close() expression will raise an
exception. So we need another try-except block here.
The block of lines from 11 to 15 is the inner try block. We will
try to close the file in a try block and if it succeeds then it will
close it. If not, the except block in line 14 will be executed. But
there is nothing to do here, so it will simply pass .
That’s how we can use nested try blocks and be sure that our
code can handle all possible exceptions. Before calling the function
let’s modify the content of the example.txt file. We will add the
finally block. Here is the final content:

In Python try-except-else structure:

try:
<---- code ---->
except:
<---- error ---->
else:
<---- no errors ---->
finally:
<---- in any case ---->

Let’s call the function and see the output. First let’s call with a
valid path:

[40]: 1 # call with a valid path


2 open_file('example.txt')
[40]: In Python try-except-else structure:

try:
<---- code ---->
except:
<---- error ---->
else:
<---- no errors ---->
finally:
<---- in any case ---->
Closing the file.

As you see in cell 40, our function opens the file and prints its
content successfully. And then it closes the file. Let’s call it with a
wrong file path now:

[41]: 1 # call with a wrong path


2 open_file('exampleeee.txt')

[41]: [Errno 2] No such file or directory: 'exampleeee.txt'

As you see in cell 41, our function simply printed the error text
and finalized execution. This is the text we print in the except
statement in line 7.
Now that you learned Exception Handling in Python, it’s time
to have quiz on it. You will have a quiz of 10 questions, and you
are expected to solve the question on your own. The solutions are
provided right after the questions.
QUIZ - Exception Handling
Now it’s time to solve the quiz for this chapter. You can
download the quiz file, QUIZ_Exception_Handling.zip, from the
Github Repository of this book. It is in the folder named
3_Exception_Handling. You should put the quiz file in the
Exception_Handling project we build in this chapter.

Figure 3-4: QUIZ_Exception_Handling.py file in the project

Here are the questions for this chapter:

QUIZ - Exception Handling:

Q1:

Define a function named is_list.


It will take a list as the parameter.
And it will check if this parameter is actually of list type or not.
If the type of the parameter is not List, it will raise an exception
as:
"The parameter type is not List."
If the type of the parameter is List, then it will print the
parameter and 'pass'.

Hints:
assert
pass
do not use try-except

Expected Output:
# call with a Tuple
is_list(('a', 'b', 'c'))
Output: AssertionError: The parameter type is not List.

# call with a List


is_list(['a', 'b', 'c'])
Output: ['a', 'b', 'c']

[1]: 1 # Q 1:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 # call with a Tuple
7 is_list(('a', 'b', 'c'))
8
9 # call with a List
10 # is_list(['a', 'b', 'c'])

[1]: AssertionError: The parameter type is not List.

Q2:

Define a function named sum_of_list.


It will take a list as parameter and calculate the sum of numbers
in this list.
If it encounters an error during summation, it will raise an
Exception.
Otherwise, it will return the summation.
The exception type will be 'TypeError' and the text will be the
standard error text.

Hints:
try-except
except .... as ....

Expected Output:
print(sum_of_list([1, 'b', 3]))
Output: TypeError: unsupported operand type(s) for +=: 'int' and
'str'

print(sum_of_list([1, 2, 3]))
Output: 6

[2]: 1 # Q 2:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 print(sum_of_list([1, 'b', 3]))
7 # print(sum_of_list([1, 2, 3]))

TypeError: unsupported operand type(s) for +=: 'int'


[2]:
and 'str'

Q3:

Define a function named sum_of_list_else.


It will take a list as parameter and calculate the sum of numbers
in this list.
If it encounters an error during summation, it will raise an
Exception.
Otherwise, it will return the summation.
The exception type will be 'TypeError' and the text will be the
standard error text.

Hints:
try-except-else
except .... as ....

Expected Output:
print(sum_of_list_else([1, 'b', 3]))
Output: TypeError: unsupported operand type(s) for +=: 'int' and
'str'

print(sum_of_list_else([1, 2, 3]))
Output: 6

[3]: 1 # Q 3:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 print(sum_of_list_else([1, 'b', 3]))
7 # print(sum_of_list_else([1, 2, 3]))

TypeError: unsupported operand type(s) for +=: 'int'


[3]:
and 'str'

Q4:

Define a function named sum_of_numbers_in_list.


It will take a list as the parameter.
And it will add the numbers in that list.
If any item is not a number it will ignore that item, but it will
print as:
"item 'x' is not a number"
The function will return the summation result.

Hints:
try-except-else
assert
pass
try-except-else inside for loop

Expected Output:
sum_of_numbers_in_list([1, 'a', 'b', 3])
Output:
item a is not a number
item b is not a number
4

print(sum_of_numbers_in_list([1, 2, 3]))
Output: 6

[4]: 1 # Q 4:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 print(sum_of_numbers_in_list([1, 'a', 'b', 3]))
7 # print(sum_of_numbers_in_list([1, 2, 3]))

[4]: item a is not a number.


item b is not a number.
4

Q5:
Below, you see a function named total_likes.
This function creates a dictionary of reviews.
And it returns total number of likes in this dictionary.
But there is a problem.
If you run the function as it is currently, you will see it raises
an exception.
Fix this bug in the function by using try-except-else structure.

Hints:
examine each review carefully :)

def total_likes():
reviews = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

for review in reviews:


total += review[ 'Like']

return total

Expected Output:
total_number_of_likes = total_likes()
print(total_number_of_likes)
Output: 45

[5]: 1 # Q 5:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 total_number_of_likes = total_likes()
7 print(total_number_of_likes)

[5]: 45

Q6:

Define a function named calculator.


It will ask for a mathematical operation from the user.
An operation is something like this: 6 * 5
The function will get this input and do the necessary
calculation, and return the result as follows:
6 * 5 = 30
The operation type can only be one of these: +, -, *, /
Possible errors during the function execution:
1- The user might not include spaces between elements.
- You must check the number of elements in the input -> 6*5
2- The user might use an operation type which is included in (+,
-, *, /).
- Check operation type
3- The first item and the third item (the operands) should be
cast into float.
- Check if the operands can cast to float -> 6 * x
4- If the operation type is division (/) then the 3rd element
cannot be zero (0).
- Check for the second operand not to be zero. -> 8 / 0
Define a working calculator by taking these possibilities into
account.
Use try-except-else structure.

Expected Output:
result = calculator()
print(result)
Output:
Please enter an operation: 4 / 0
AssertionError: Divisor cannot be zero!

[6]: 1 # Q 6:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 result = calculator()
7 print(result)

[6]: Please enter an operation: 4 / 0


AssertionError: Divisor cannot be zero!

Q7:

Define a function named give_me_a_key.


It will ask for the user to press any key on the keyboard.
If the key that the user pressed is:
1- a number -> return its square
2- a letter -> return its capital form
3- neither a number nor a letter -> return the key itself
Define this function by using try-except-else-finally structure.

Expected Output:
give_me_a_key()
Output:
Please give me a key: b
B

[7]: 1 # Q 7:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 give_me_a_key()

[7]: Please give me a key: b


B

Q8:

Define a function named item_at_index.


It will take a list and an index as parameters.
It will search for the item at this index in the list.
If it doesn't find the item, it will raise a standard exception.
If it finds, it will return the result.

Hints:
try-except-else
raise only (no exception type declaration)

Expected Output:
my_list = ['x', 'y', 'z', 't']
ind = 7
result = item_at_index(my_list, ind)
print(result)
Output: IndexError: list index out of range

[8]: 1 # Q 8:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 my_list = [ 'x', 'y', 'z', 't']
7 ind = 7
8 result = item_at_index(my_list, ind)
9 print(result)
[8]: IndexError: list index out of range

Q9:

Define a function named file_reader.


It will take a file path as the parameter.
If the file doesn't exist at the specified path it will raise an
exception.
Exception type will be standard exception.
It the file exists, it will read the content and print.
In any case (finally) it will close the file.

Hints:
try-except-else-finally
you need to check for the file in finally block

Expected Output:
file_reader('serieeees.txt')
Output: FileNotFoundError: [Errno 2] No such file or directory:
'serieeees.txt'

[9]: 1 # Q 9:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 # file which doesn't exist
7 path = "serieeees.txt"
8 file_reader(path)
9
10 # file which exists
11 # path = "series.txt"
12 # file_reader(path)
FileNotFoundError: [Errno 2] No such file or
[9]:
directory: 'serieeees.txt'

Q10:

We have a dictionary of TV series and number of seasons.

series = {
'Game of Thrones': 8,
'Fargo': 4,
'Dark': 3,
'True Detective': 3,
'Dogs of Berlin': 1,
'The Crown': 6
}

Define a function named series_and_seasons that will use this


dictionary.
It will ask for the name of a series and check if this series exists
in the dictionary.
If it is not in the series dictionary the function will raise an
exception as:
'No such series in the dictionary. Please try again: '
And it will ask again and again.
It will ask until it finds the given name in the dictionary.
Otherwise it will keep asking :)

Hints:
while (infinite loop)
try-except-else-finally

Expected Output:
series_and_seasons()
Output:
Please enter a series name: Queens Gambit
No such series in the dictionary. Please try again:
Please enter a series name: Squid Game
No such series in the dictionary. Please try again:
Please enter a series name: Dark
number of seasons in Dark is: 3
Congratulations. You win :)

[10]: 1 # S 10:
2
3 # ---- your solution here ---
4
5 # call the function you defined
6 series_and_seasons()

[10]: Please enter a series name: Queens Gambit


No such series in the dictionary. Please try again:
Please enter a series name: Squid Game
No such series in the dictionary. Please try again:
Please enter a series name: Dark
number of seasons in Dark is: 3
Congratulations. You win :)
SOLUTIONS - Exception Handling
Here are the solutions for the quiz for Chapter 3 - Exception
Handling.

SOLUTIONS - Exception Handling:

S1:

[1]: 1 # S 1:
2 def is_list(a_list):
3 # assertion
assert type(a_list) == list, AssertionError("The
4
parameter type is not List.")
5
6 # since we passed assertion
7 print(a_list)
8 pass
9
10 # call the function you defined
11 # call with a Tuple
12 is_list(('a', 'b', 'c'))
13
14 # call with a List
15 # is_list(['a', 'b', 'c'])

[1]: AssertionError: The parameter type is not List.

S2:

[2]: 1 # S 2:
2 def sum_of_list(a_list):
3 try:
4 total = 0
5 for i in a_list:
6 total += i
7 return total
8 except TypeError as type_err:
9 raise type_err
10
11 # call the function you defined
12 print(sum_of_list([1, 'b', 3]))
13 # print(sum_of_list([1, 2, 3]))

TypeError: unsupported operand type(s) for +=:


[2]:
'int' and 'str'

S3:

[3]: 1 # S 3:
2 def sum_of_list_else(a_list):
3 try:
4 total = 0
5 for i in a_list:
6 total += i
7 except TypeError as type_err:
8 raise type_err
9 else:
10 return total
11
12 # call the function you defined
13 print(sum_of_list_else([1, 'b', 3]))
14 # print(sum_of_list_else([1, 2, 3]))

[3]: TypeError: unsupported operand type(s) for +=:


'int' and 'str'

S4:

[4]: 1 # S 4:
2 def sum_of_numbers_in_list(a_list):
3 total = 0
4
5 for i in a_list:
6 try:
7 # is this element int -> assert
8 assert int(i)
9 except:
10 print("item {0} is not a number.".format(i))
11 pass
12 else:
13 total += i
14
15 return total
16
17 # Important Note:
# this time, since we will check every element one
18 by one, we will place try-except-else inside the for
loop.
19
20 # call the function you defined
21 print(sum_of_numbers_in_list([1, 'a', 'b', 3]))
22 # print(sum_of_numbers_in_list([1, 2, 3]))

[4]: item a is not a number.


item b is not a number.
4
S5:

[5]: 1 # S 5:
2 def total_likes():
reviews = [{ 'Image': 4, 'Like': 20, 'Comment':
3
12},
4 {'Like': 15, 'Comment': 8, 'Share': 10},
5 {'Image': 7, 'Comment': 16, 'Share': 37},
6 {'Image': 6, 'Like': 10, 'Comment': 9}]
7
8 total = 0
9
10 for review in reviews:
11 try:
12 like = review[ 'Like']
13 except:
14 # moves to the next element in the loop
15 continue
16 else:
17 total += like
18
19 return total
20
21 # call the function you defined
22 total_number_of_likes = total_likes()
23 print(total_number_of_likes)

[5]: 45

S6:

[6]: 1 # S 6:
2 def calculator():
3
4 user_input = input('Please enter an operation: ')
5
6 # first let's split user_input
7 elements = user_input .split()
8
9 # check length of elements
10 if len(elements) != 3:
raise Exception('Please enter two operands and
11
an operation type, separated by space.')
12
13 # check for operation type
14 operations = ( '+', '-', '*', '/')
15 operation_symbol = elements[ 1]
16 if not operation_symbol in operations:
raise Exception('{0} type, is not valid. It must
17
be in {1}'.format(operation_symbol, operations))
18
19 # operands check
20 try:
21 num_1 = float(elements[0])
22 num_2 = float(elements[2])
23 except:
24 raise Exception('Operands must be numeric!')
25 else:
26 # so far all is OK -> operands are numeric
27 if operation_symbol == '+':
28 result = num_1 + num_2
29 if operation_symbol == '-':
30 result = num_1 - num_2
31 if operation_symbol == '*':
32 result = num_1 * num_2
33 if operation_symbol == '/':
34 assert num_2 != 0, 'Divisor cannot be zero!'
35 result = num_1 / num_2
36
return "{0} {1} {2} = {3}".format(num_1,
37
operation_symbol, num_2, result)
38
39 # call the function you defined
40 result = calculator()
41 print(result)

[6]: Please enter an operation: 4 / 0


AssertionError: Divisor cannot be zero!

S7:

[7]: 1 # S 7:
2 def give_me_a_key():
3
4 char = input('Please give me a key: ')
5 result = ''
6
7 # check if int
8 try:
9 int(char)
10 except:
11 # it is not int
12 # check for letter -> isalpha()
13 try:
14 assert char .isalpha()
15 result = char .upper()
16 except:
17 # it is not a letter
18 result = char
19 else:
20 # it is int
21 result = int(char)**2
22 finally:
23 print(result)
24
25 # call the function you defined
26 give_me_a_key()

[7]: Please give me a key: b


B

S8:

[8]: 1 # S 8:
2 def item_at_index(a_list, index):
3 try:
4 item = a_list[index]
5 except IndexError as ie:
6 raise
7 else:
8 return item
9
10 # call the function you defined
11 my_list = [ 'x', 'y', 'z', 't']
12 ind = 7
13 result = item_at_index(my_list, ind)
14 print(result)

[8]: IndexError: list index out of range

S9:
[9]: 1 # S 9:
2 def file_reader(path):
3 try:
4 file = open(path)
5 except FileNotFoundError:
6 raise
7 else:
8 print(file.read())
9 finally:
10 try:
11 file.close()
12 except:
13 pass
14
15 # call the function you defined
16 # file which doesn't exist
17 path = "serieeees.txt"
18 file_reader(path)
19
20 # file which exists
21 # path = "series.txt"
22 # file_reader(path)

FileNotFoundError: [Errno 2] No such file or


[9]:
directory: 'serieeees.txt'

S10:

[10]: 1 # S 10:
2 def series_and_seasons():
3 series = {
4 'Game of Thrones': 8,
5 'Fargo': 4,
6 'Dark': 3,
7 'True Detective': 3,
8 'Dogs of Berlin': 1,
9 'The Crown': 6
10 }
11
12 while True:
13 try:
14 name = input('Please enter a series name: ')
print('number of seasons in {0} is:
15
{1}'.format(name, series[name]))
16 except:
print('No such series in the dictionary.
17
Please try again: ')
18 else:
19 break
20
21 print('Congratulations. You win :)')
22
23 # call the function you defined
24 series_and_seasons()

[10]: Please enter a series name: Queens Gambit


No such series in the dictionary. Please try again:
Please enter a series name: Squid Game
No such series in the dictionary. Please try again:
Please enter a series name: Dark
number of seasons in Dark is: 3
Congratulations. You win :)
4. Modules & Packages
This chapter is about Modules and Packages in Python.
Python is modular in nature and Modular Programming has many
advantages. First of all Modular Programming prevents code
repetition. You define a module once and you can use it whenever
you need. Second, it provides a better organization for your files.
For example;
files related to the web interface can be stored in a package
called web,
database related files can be in a package called db,
files which are used for api connections can be stored in a
package called api, and so on.
Third, Modular Programming approach will provide easy
maintenance of the code.
In this chapter you will learn how to use standard modules, how
to define custom modules and how to pack modules in packages.
We will create a new PyCharm project for this chapter. The
project name is Modules_and_Packages. You are recommended
to create a new project with a new virtual environment. You can
also download the project from the Github Repository of the book
if you want. Here is the project that we will develop in this chapter:
Figure 4-1: Modules_and_Packages project for this chapter
Standard Modules
A module is a file containing Python code and ending with .py
extension. Python has many built-in modules, which we call as
standard modules. Some examples are math , random ,
platform , os and sys modules. To be able to use a module in our
program we use the import keyword. Its syntax is: import
<module_name> .
We will create a new Python file for this section. The file name
is _1_standard_modules.py .
Let’s see some examples to understand how we import and use
Python modules.

math :

[1]: 1 # import the math module


2 import math
3
4 # pi number
5 pi_num = math .pi
6 print(pi_num)

[1]: 3.141592653589793

In cell 1, we import a module named math . This module


provides access to the mathematical functions. In line 5, we get the
pi number as: math.pi .

random :
The random module implements pseudo-random number
generators for various distributions. This module helps us to get a
random probability between 0 and 1, a random integer in an
interval, or a sample from a list.

[2]: 1 # random module


2 import random
3
4 # 0 - 1
5 probability = random .random()
6 print(probability)

[2]: 0.8495443771116593

In cell 2, we import the random module as: import random.


In line 5, we call the random() function of the random module
as: random.random() . This function returns a probability value
between 0 and 1.

[3]: 1 # random number


2 rand_num = random .randint(10, 50)
3 print(rand_num)

[3]: 12

In cell 3, we use the randint() function in the random


module. The syntax is: randint(start, end). It returns a random
integer N such that start <= N <= end. Both of the start and the
end must be integer type values. In line 2 we call it as:
random.randint(10, 50). And the result is 12 .

[4]: 1 # a random element from a list


2 a_list = [ 1, 2, 3, 4, 5, 6, 7, 8, 9]
3 rand_item = random .choice(a_list)
4 print(rand_item)
[4]: 6

In cell 4, we use the random.choice() function. Its syntax is:


random.choice(seq) . And it returns a random element from the
non-empty sequence seq . If seq is empty, raises IndexError . In
line 3, we pass the a_list variable as the sequence and it returns 6 .

[5]: 1 # a random range -> sample


2 rand_sample = random .sample(a_list, 3)
3 print(rand_sample)

[5]: [5, 1, 9]

In cell 5, you see another function from the random module


which is sample() . Its syntax is: random.sample(population, k,
*, counts=None). It returns a k length list of unique elements
chosen from the population sequence or set. Used for random
sampling without replacement. It returns a new list containing
elements from the population while leaving the original population
unchanged.
In line 2, we use the sample() function as:
random.sample(a_list, 3). We pass the a_list variable which we
defined in cell 4 for the population. And for the value of k , we
pass 3 . The result is a list of [5, 1, 9].

platform :
The platform module is used to access to the underlying
platform’s identifying data, such as architecture, machine,
processor, system, version, etc. Let’s import this module and print
it to see the basic data about it.
[6]: 1 import platform
2 print(platform)

<module 'platform' from


[6]:
'...Python\Python39\lib\platform.py'>

In cell 6, we import and print the platform module. Python


prints its name and location in the output.

[7]: 1 # platform type


2 print('platform type:', platform.platform())

[7]: platform type: Windows-10-10.0.19041-SP0

In cell 7, we print the platform type of the machine which the


current Python code runs.

[8]: 1 # platform architecture


print('platform architecture:',
2
platform.architecture())

[8]: platform architecture: ('64bit', 'WindowsPE')

In cell 8, we print the platform architecture of the machine


which this Python code runs. Let’s print the machine data and the
operating system name:

[9]: 1 # machine data


2 print('machine:', platform.machine())

[9]: machine: AMD64


[10]: 1 # OS data
2 print('OS:', platform.system())

[10]: OS: Windows

os :
This module provides a portable way of using operating system
dependent functionality.

[11]: 1 import os
2
3 # current folder of our project
4 print(os.getcwd())

[11]: ...\4_Modules_and_Packages\Modules_and_Packages

In cell 11, we get the current working directory as:


os.getcwd() . It return a string representing the current folder of the
project.

[12]: 1 # logged in user


2 print(os.getlogin())

[12]: <your_user_name>

In cell 12, we get the name of the logged in user as:


os.getlogin() .

sys :
This module provides access to some variables used or
maintained by the interpreter and to functions that interact strongly
with the interpreter. It is always available.
One of the most important variables in the sys module is the
sys.path variable. This variable is a list of strings that specifies the
search path for modules. In other words, Python Interpreter uses
the sys.path variable to find the modules. Let’s print all the paths
in sys.path list with a for loop:

[13]: 1 import sys


2
3 # path variable
4 paths = sys .path
5
6 print('Python Search Paths for Modules:')
7 for path in paths:
8 print(path)

[13]: Python Search Paths for Modules:


...\4_Modules_and_Packages\Modules_and_Packages
...\4_Modules_and_Packages\Modules_and_Packages\venv
...\4_Modules_and_Packages\Modules_and_Packages\venv\lib\sit
packages
...\Python\Python39\python39.zip
...\Python\Python39\DLLs
...\Python\Python39\lib
...\Python\Python39

You see the list of paths in the sys.path variable in the output
of cell 13.

Set Custom Name for a Module at Import:


Sometimes you need to give a custom name to the module you
import. It helps you avoid name clashes. The syntax is: import
module as <custom_name>.
Let’s import the random module and give it a custom name as
rnd :

[14]: 1 # give a custom name


2 import random as rnd
3 print(rnd.random())

[14]: 0.47178217055015603

In cell 14, we rename the imported module random as rnd ,


and rnd means the random module in the current project, from
now.

Importing Multiple Modules at Once:


It is possible to import more than one modules with a single
import statement. Here is an example:

[15]: 1 # import multiple modules


2 import random, math, sys, os

As you see in cell 15, we import multiple modules in a single


import. All we have to do is, to separate the module names with
commas.

Importing Sub Modules or Objects (Variables, Functions


etc.):
In Python, we don’t have to import the whole module if we
don’t need it. We can import a sub module or any of the objects
(variables, functions etc.) in the module. The syntax is: from
<module> import <something>.
Let’s import only one function, randint , from the random
module and use it:
[16]: 1 # import single function
2 from random import randint
3 a_random_number = randint( 1, 11)
4 print(a_random_number)

[16]: 8

In cell 16, we import the randint function from the random


module and we generate a number between 1 and 11 .
Now let’s import a single dictionary object from the sys
module. The dictionary name is modules and it maps module
names to modules which have already been loaded.

[17]: 1 # import a dictionary


2 from sys import modules
3 print(modules)

{'sys': <module 'sys' (built-in)>, 'builtins': <module


[17]:
'builtins' (built-in)>, … }

Custom Names for the Imported Objects:


We can give custom names for the objects we import. The
syntax is from <module> import <something> as
<custom_name> . Let’s see an example:

[18]: 1 # custom name for the imported object


2 from math import sqrt as sq
3 print(sq(16))

[18]: 4.0
In cell 18, we import the sqrt() function from the math
module and we name it as sq . Now, we can use the sq function to
take the square root of numbers. In line 3, we print the square root
of 16 as sq(16) and the result is 4.0 .

Wildcard Imports:
We can import all of the module content with the wildcard
character which is star (*). The syntax is: from module import *.
When you use the wildcard character (*) for import, Python
imports all the content of that module. But this type of import may
cause name clashes which are conflicts between names defined
locally and the ones imported. So you should try to avoid wildcard
imports.
Let’s see an example of how wildcard imports can be
dangerous. Let’s say, we import the random module with
wildcard. Then we define a custom function named randint .

[19]: 1 # this leads name clash


2 from random import *
3
4 # define a function named randint
5 def randint():
6 print('ABC')
7
8 # call the randint function
9 print(randint(10,20))

TypeError: randint() takes 0 positional arguments


[19]:
but 2 were given

In cell 19, we import the random module as wildcard. And we


define a custom function named randint . As we already know the
random module also has a function with this name. As you see in
the output, we get an error when we try to call the randint()
function in the random module, because we override it.
Defining Custom Modules
It is very easy to define custom modules in Python. Remember
that, a module is a file that contains Python code and ends with .py
extension. In our project for this chapter, we will create a module
named first_module.py . To do this in PyCharm, right click on the
project name and then select New > Python File. Type the file
name and press Enter.

Figure 4-2: Creating a custom module

Here is the project structure with all the files:


Figure 4-3: Project Structure and the first module

And here is the content of the first_module.py file:

[20]: 1 """
2 this module prints:
3 'Hello Python Module' statement.
4 """
5
6 def greeting():
7 print('Hello Python Module')

In cell 20, you see the code in the first_module.py file. At the
top, we have some basic information about the module, in form of
a multi-line comment. And in line 6, we have a function definition.
The function name is greeting and it prints the text of 'Hello
Python Module'. Now, we will call this function from another file.
We will create a new Python file for this section. The file name
is _2_define_custom_modules.py . In this new file we will import
our first_module . Then we will call the greeting function in that
module. Let’s do it:

[21]: 1 # import our first module


2 import first_module

In cell 21, we import the first_module which we created


earlier. Now we can call the function in that module.

[22]: 1 # call the function in module


2 first_module.greeting()

[22]: Hello Python Module

In cell 22, we call the greeting function in the first_module


as: first_module.greeting() . And it prints the text of ‘Hello
Python Module’.
Now let’s create a new module. We will name it as
input_operations.py and here is the content of this module:

[23]: 1 """
2 module: input_operations
3 This module is for input operations.
4 """
5
6 def get_input(type='text'):
return input('Please enter a/an {0}:
7
'.format(type))
8
9 def get_integer():
10 while True:
11 try:
12 user_input = get_input( type='integer')
13 num = int(user_input)
14 except:
15 continue
16 else:
17 return num
18
19 def get_float():
20 while True:
21 try:
22 user_input = get_input( type='float')
23 flt = float(user_input)
24 except:
25 continue
26 else:
27 return flt

In cell 23, you see the content of the input_operations


module. We have three functions in this module which are
get_input , get_integer and get_float . As their names imply,
they try to get specific types of inputs from the user.
get_integer and get_float functions keep asking for user
inputs until they get int or float types. They have the same
pattern of try-except-else structure. Let’s take get_integer
function for example. The idea of get_float function is almost the
same.
The get_integer function starts with an infinite while loop as:
while True. Which mean it intends to run forever. Inside the while
loop there is a try block.
The try block is where we ask for an input from the user. To
do this we call the get_input function and pass the type as the
parameter as: get_input(type='integer') . The get_input
function returns the user input and we try to convert it to an integer
as: num = int(user_input).
If we succeed to convert the user input to an int , then we
assign the resulting integer number to a variable called num . Since
the try block runs successfully, the else block executes and it
returns the num variable. Which also terminates the while loop
and the function execution.
If we cannot convert the user input to an int , in other words, if
we get an error in line 13, then the except block executes. There is
a simple continue statement in the except block, which means
the while loop will keep iteration.
The get_float function has exactly the same structure. The
only difference is that, it checks for floats instead of integers.
Now that we know all about the input_operations module
let’s import it into our _2_define_custom_modules and rename it
as io :

[24]: 1 # import module


2 import input_operations as io

Now let’s call the functions in the input_operations module,


which we simple name as io in the current file.

[25]: 1 # call the get_input function


2 text_input = io .get_input()
3 print(text_input)

[25]: Please enter a/an text: This is my second module


This is my second module
In cell 25, we call the get_input function from the
input_operations (io) module as: io.get_input() . And we print
the resulting text from the function.

[26]: 1 # call the get_integer function


2 user_integer = io .get_integer()
3 print(user_integer)

[26]: Please enter a/an integer: asd


Please enter a/an integer: qwerty
Please enter a/an integer: abc
Please enter a/an integer: 567
567

In cell 25, we call the get_integer function as:


io.get_integer() . And it keeps asking for an integer until we pass a
numeric value.
As the final example in this section let’s call the get_float
function of the input_operations module:

[27]: 1 # call the get_float function


2 user_float = io .get_float()
3 print(user_float)

[27]: Please enter a/an float: arda


Please enter a/an float: xyz
Please enter a/an float: 7.8
7.8
How Python Find Modules?
When we import a module in our program Python finds and
brings that module for us. Knowing how Python finds modules is
important to be able manage large applications.
Here are the ways how Python looks for modules:
1- It searches the current folder that import <module_name>
code runs. This is the same level with the running file.
2- It checks the PYTHONPATH environment variable, if that
variable is defined at system level. PYTHONPATH is an
environment variable which you can set to add additional
directories for your custom modules and packages. It is not
needed for standard Python modules and packages, because
Python knows where to find its standard library.
3- It searches the folders where Python is installed (virtual
environments).

This might seem complex at first but don’t worry, there is an


easy way to list the directories where Python looks for modules.
We have a special variable in the sys module for this purpose
which is: sys.path . This variable displays all of the Python search
paths for modules and packages.
We will create a new Python file for this section. The file name
is _3_how_python_finds_modules.py . Let’s print the items in
the sys.path variable:

[28]: 1 # import sys module


2 import sys
3
4 # Search Paths of Python
5 python_search_path = sys .path
6
7 # print the paths
8 for path in python_search_path:
9 print(path)

[28]: ...\4_Modules_and_Packages\Modules_and_Packages
...\AppData\Local\Programs\Python\Python39\python39.zip
...\AppData\Local\Programs\Python\Python39\DLLs
...\AppData\Local\Programs\Python\Python39\lib
…\AppData\Local\Programs\Python\Python39
...\4_Modules_and_Packages\Modules_and_Packages\venv
...\4_Modules_and_Packages\Modules_and_Packages\venv\lib\sit
packages

In cell 28, we import the sys module and get the search paths in
line 5 as: python_search_path = sys.path. Then in line 8, we
print the paths with the help of a for loop. And you see the list of
all directories where Python search for modules and packages.

Directory Access:
To be able to import a module in a directory in the same
project, we use the syntax of: <dir_name>.<module_name> .
Let’s create a folder in the current project. The folder name will
be modules . To create a directory (folder) in PyCharm, right click
on the project name and then select New > Directory. Type the
name as modules and press Enter.
Figure 4-4: Create a directory (folder) in PyCharm

Once your new directory is created, copy the


input_operations.py file and paste it into the modules folder.
Rename it as module_input_operations.py to avoid name
clashes.
Figure 4-5: module_input_operations.py file in the modules folder

Now, let’s import the module_input_operations module in


the modules directory.

[29]: 1 # directory access (dir_name.module_name)


2 import modules.module_input_operations
3
4 # call the get_input function
user_input =
5
modules.module_input_operations.get_input()
6 print(user_input)

Please enter a/an text: This is from the module in a


[29]:
folder
This is from the module in a folder
In cell 29, we import the module_input_operations from the
modules directory as: modules.module_input_operations . Then
in line 5, we call the get_input function as:
modules.module_input_operations.get_input() .

Project Level Access:


In general, it is not a good practice to get modules with folder
path. Because folder names or path may change. Let’s assume we
need to import a module in more than one code files. Instead of
putting this module in a separate folder, we should keep it in
central directory where all the files in the current project can access
easily. This central directory is the \venv\lib\site-packages
folder. Be careful that, your project need to use the virtual
environment in the venv folder. Otherwise Python will not look
into the \venv\lib\site-packages folder. Remember in cell 28, this
is one of the places in the sys.path list.
Now, copy the input_operations.py file and paste it into the
\venv\lib\site-packages folder. Rename it as
local_module_input_operations.py .

Figure 4-6: Copy the module_input_operations.py file in \venv\lib\site-


packages folder
Now that our module is in the \venv\lib\site-packages
directory, we can import it easily by just typing its name in in our
code file. Let’s do it:

[30]: 1 """
2 Project level access:
3 Modules_and_Packages\venv\lib\site-packages
4 """
5
6 import local_module_input_operations
7
user_input =
8
local_module_input_operations.get_input()
9 print(user_input)

Please enter a/an text: Project Level access to a


[30]:
module
Project Level access to a module

As you see in cell 30, we import the


local_module_input_operations module by simply typing its
name. We do not need any folder path because it is in the central
directory for modules in this project.

System Level Access:


In some cases, you need to access a module from any project on
your machine. This is called global access and your module
becomes accessible to any Python file on the same system where
Python runs. To be able to make a module globally accessible you
need to put it in one of the directories where Python is installed.
You can see the list of these directories in the sys.path list. When
you check the output of cell 28, where we print the items in the
sys.path , you will see that there is a path as:
...\AppData\Local\Programs\Python\Python39\lib . This path
(on a Windows machine) is where you should keep your custom
modules to make them global.
Let’s copy the input_operations.py file and paste it into the
...\AppData\Local\Programs\Python\Python39\lib folder.
Rename it as global_input_operations.py .

Figure 4-7: global_input_operations module in


...\AppData\Local\Programs\Python\Python39\lib folder

Now let’s import the as global_input_operations module in


our code file and call the get_integer function in it:

[31]: 1 """
2 System level access (global access):
3 ...\Python\Python39\lib
4 """
5
6 import global_input_operations
7
8 print(global_input_operations.get_integer())

[31]: Please enter a/an integer: abc


Please enter a/an integer: 789
789

In cell 31, we import the global_input_operations module


and call a function in it. Since it’s in the global directory for Python
modules and packages, we do not need declare any path for it. We
just type its name and that’s all.
Packages
Packages are great in computer programming because they
provide lots of advantages. Some of these advantages are, they help
us to organize the code, they are portable and they are a good way
to arrange modules.
What is a Python package? Actually, a Python package is a
special kind of a directory. The main difference between a Python
package and a regular folder (directory) is that the Python package
includes __init__.py file. This __init__.py file tells Python that
the current directory is a package.
A Python package can include one or several modules or other
packages. You can nest packages inside packages.
To create a Python package in PyCharm, right click on the
project name and select New > Python Package. Type the name
of the package as pack and press Enter.

Figure 4-8: Creating a Python package in PyCharm

When you create a Python package, PyCharm automatically


creates an empty __init__.py file. You can think of this file as the
entry point for the package. We fill this file in a minute.
Let’s create two modules in our pack package. The name of
the first module is mod_1.py and here is the content of it:

[32]: 1 """
2 module: mod_1
3 """
4
5 def print_mod_1():
6 print('Module 1')

The name of the first module is mod_2.py and here is the


content of it:

[33]: 1 """
2 module: mod_2
3 """
4
5 def print_mod_2():
6 print('Module 2')
Figure 4-9: The pack package and the modules in it

Now that we have a custom package, let’s create the Python file
for this section. The file name is _4_packages.py and it will use
the modules in the pack package.
To access a module under a package, we first type the package
name then the module name. Here is the syntax:
<package_name>.<module_name> .

[34]: 1 # import the modules in the pack package


2 import pack.mod_1, pack.mod_2
3 pack.mod_1.print_mod_1()
4 pack.mod_2.print_mod_2()

[34]: Module 1
Module 2
In cell 34, we import the mod_1 and mod_2 modules in the
pack package as: import pack.mod_1, pack.mod_2. Then we
call the functions in these modules. Be careful that we import only
the individual modules here, not the pack package as a whole.

__init__.py :
The __init__.py files are required to make Python treat
directories containing this file as packages. This prevents
directories with a common name, such as string, unintentionally
hiding valid modules that occur later on the module search path. In
the simplest case, __init__.py can just be an empty file, but it can
also execute initialization code for the package.
In general __init__.py files contain:
sub-packages and imports for modules
global variables
documentation
Now let’s modify the __init__.py file in the pack package. It
will import the modules which are in the current package. Here is
the file content:

[35]: 1 """
2 __init__.py
3 The pack package has two modules:
4 * mod_1
5 * mod_2
6 """
7
8 # absolute import
9 # wrong way
10 # import mod_1
11 # import mod_2
12
13 # relative import
14 # correct way
15 from . import mod_1
16 from . import mod_2

In cell 35, you see the content of the __init__.py file. It


imports the modules in this package. There are two ways to import.
absolute import:
The first one (lines 10 and 11) is the absolute import which you
should avoid. You will get an error, ModuleNotFoundError ,
when you import the pack package later on. So avoid using
absolute imports in the __init__.py files.
relative import:
The second way to import the modules in a package is the
relative import. It’s syntax is: from . import <module>. Here the
dot (.) means that the module is in the current package. This is the
correct way to import modules or sub-packages in a package.
Now let’s import the pack package again in our
_4_packages.py file. This time we will import the package itself
as: import pack. Then we can access the modules in the package.
Here is the code:

[36]: 1 # import the package


2 import pack
3
4 # access the modules
5 pack.mod_1.print_mod_1()
6 pack.mod_2.print_mod_2()

[36]: Module 1
Module 2
Installing Packages
In this section we will learn how to install third party packages
into our Python environment. There are many different ways to
install packages. The most common way is to use pip. Pip is a
package itself and serves as the package manager for Python
packages and modules. It is installed by default when you install
Python (starting with Python 3.4) or when you start a new virtual
environment. Here is the documentation about how to use pip.[6]
PyPI: The Python Package Index (PyPI) is the central repository
of software for the Python programming language. PyPI helps you
find and install software developed and shared by the Python
community.
However we will not use pip in this book. Instead, we will use
PyCharm to install packages for us. Since we develop our projects
in PyCharm, it is safer to let PyCharm to handle package
installation including all the dependencies etc.
Let say we want to install a package for PDF operations. The
package name is PyPDF2 and it’s very common for PDF related
tasks in Python. Actually we will need this package later on in this
book. Let’s install this package to our project with PyCharm.
In PyCharm, under the main menu select File and then
Settings. It will open up the Settings window. Here, under Project,
click on Python Interpreter. See the image below:
Figure 4-10: Packages in the Settings window

In the Settings window, you see all the external packages which
are installed in the current virtual environment. Remember that,
every project may have a separate virtual environment with
different packages and package versions. You see the pip package
as the first one, which is installed by default.
Now we want to install a new package. To do this, click on the
plus ( + ) icon on the package list. It will open the Package
Installer window. Here you have to type the package name, which
is PyPDF2 in our case. PyCharm will search and find the package
with its official documentation.
Figure 4-11: Package Installer window

When you click on the Install Package button, PyCharm will


install it and you will be able to see this package in the package list
of your project. PyCharm will also install necessary packages
which this package is dependent. That we call dependency
management for packages.
This is how we install third party Python packages in PyCharm.
QUIZ - Modules and Packages
Now it’s time to solve the quiz for this chapter. You can
download the quiz file, QUIZ_Modules_and_Packages.zip, from
the Github Repository of this book. It is in the folder named
4_Modules_and_Packages. You should put the quiz file in the
Modules_and_Packages project we build in this chapter.

Figure 4-12: QUIZ_modules_and_packages.py file in the project

Here are the questions for this chapter:

QUIZ - Modules and Packages:

Q1:

Define a function named random_printer.


It will import the random module and will print a random
number between 100 and 200.

Hints:
randint()

[1]: 1 # Q 1:
2
3 #---- your solution here ----
4
5 # call the function you defined
6 random_printer()

[1]: 136

Q2:

Define a function named random_list.


It will import random module with custom name rnd.
And it will create a list of size 10.
The items in the list are random values between 100 and 200.
The function will print the list all at once.
Then it will randomly select 4 of the items in this list and print
them.

Hints:
randint()
sample()

[2]: 1 # Q 2:
2
3 #---- your solution here ----
4
5 # call the function you defined
6 random_list()
[2]: [160, 119, 172, 110, 167, 132, 152, 127, 135, 200]
[160, 127, 172, 200]

Q3:

Print the current project path via the os module.

Hints:
getcwd()

[3]: 1 # Q 3:
2
3 #---- your solution here ----

Q4:

Print your computers:


Operating System
Processor
using standard Python modules.

Hints:
* platform

[4]: 1 # Q 4:
2
3 #---- your solution here ----

Q5:

Print the Python Search Path via the sys module.


Python Search Path is the list of paths which Python uses when
looking for modules.
Print the items one by one by the help of a for loop.
Hints:
path

[5]: 1 # Q 5:
2
3 #---- your solution here ----

Q6:

Create a module named consonants in the current project.


Define a function named get_consonants in this module.
This function takes a text as parameter and it will return a set of
consonants in this text.
Call this function with a text and print the consonants in this
text.

Hints:
your module should implement its own exception handling

Expected Output:
'Pyton Programming Language.... /@*-'
Consonants: {'r', 'P', 'L', 'y', 'm', 'n', 't', 'g'}

[6]: 1 # Q 6:
2 #-- create the consonants module first --
3
4 # import the module you create
5 import consonants
6
7 text = 'Pyton Programming Language A.... /@*-'
8 cons = consonants .get_consonants(text)
9 print(cons)
[6]: {'P', 'L', 't', 'r', 'g', 'm', 'n', 'y'}

Q7:

Create a module named vowel and make it available for all files
in this project.
In this module define a function named get_vowels.
This function takes a text as parameter and it will return a set of
vowels in this text.
Call this function with a text and print the vowels in this text.

Hints:
your module should implement its own exception handling
inspect sys.path
venv/lib/site-packages

Expected Output:
'Pyton Programming Language.... /@*-'
Vowels: {'a', 'e', 'i', 'o', 'u'}

[7]: 1 # Q 7:
2
3 #-- create the vowel module first --
4
5 import vowel
6
7 text = 'Pyton Programming Language .... /@*-'
8 vows = vowel .get_vowels(text)
9 print(vows)

[7]: {'a', 'i', 'o', 'u', 'e'}

Q8:
Create a Python Package named quiz_packages in the current
project folder.
Copy the modules you created in Q6 (consonants) and Q7
(vowel) in this package.
Import these modules from quiz_packages package and use their
functions (get_consonants, get_vowels)

Hints:
Python Package (__init__.py)
global variables in the __init__.py file.

Expected Output:
text = 'Pyton Programming Language.... /@*-'

get_consonants(text) -> {'n', 'm', 'P', 'L', 'r', 'g', 'y', 't'}
get_vowels(text) -> {'i', 'o', 'u', 'a', 'e'}

[8]: 1 # Q 8:
2
3 #-- create the quiz_packages first --
4
5 import quiz_packages
6
7 text = 'Pyton Programming Language.... /@*-'
cons =
8
quiz_packages.consonants.get_consonants(text)
9 vows = quiz_packages .vowels.get_vowels(text)
10 print(cons)
11 print(vows)

[8]: {'L', 'y', 'g', 'n', 't', 'P', 'm', 'r'}


{'i', 'o', 'a', 'u', 'e'}

Q9:
Copy the quiz_packages Python Package you created in Q8.
Now make it available for all Python projects on your machine.
In other words, make it a global package.
And rename it as quiz_packages_global.

Hints:
create a global Python Package
sys.path
to see where Python is installed on your machine:
command prompt (cmd) -> where python

Expected Output:
text = 'Pyton Programming Language.... /@*-'

get_consonants(text) -> {'r', 'P', 'y', 'm', 'n', 'L', 't', 'g'}
get_vowels(text) -> {'i', 'a', 'o', 'e', 'u'}

[9]: 1 # Q 9:
2
3 #-- create the quiz_packages_global first --
4
5 import quiz_packages_global
cons =
6
quiz_packages_global.consonants.get_consonants(text)
7 vows = quiz_packages_global .vowels.get_vowels(text)
8 print(cons)
9 print(vows)

[9]: {'n', 't', 'r', 'y', 'L', 'g', 'P', 'm'}


{'i', 'u', 'a', 'o', 'e'}

Q10:

Which one below is NOT True for Python Modules?


A- In Python, a module is a file with .py extension.
B- We use 'import' keyword to access the modules.
C- In Python, a Package is a container containing modules
D- Python packages are ordinary folders. It is enough to create a
folder to define a Python package.
SOLUTIONS - Modules and Packages
Here are the solutions for the quiz for Chapter 4 - Modules and
Packages. You can download the final project which includes the
quiz solutions from the Github Repository of this book. The file
name is Modules_and_Packages_Quiz.zip. The solutions are in
QUIZ_SOLUTION_modules_and_packages.py file.

SOLUTIONS - Modules and Packages:

S1:

[1]: 1 # S 1:
2 def random_printer():
3 import random
4 random_integer = random .randint(100, 200)
5 print(random_integer)
6
7 # call the function you defined
8 random_printer()

[1]: 180

S2:

[2]: 1 # S 2:
2 def random_list():
3 import random as rnd
4
5 the_list = []
6
7 for i in range(10):
8 random_integer = rnd .randint(100, 200)
9 the_list.append(random_integer)
10
11 print(the_list)
12
13 selected = rnd .sample(the_list, k=4)
14 print(selected)
15
16 # call the function you defined
17 random_list()

[2]: [160, 119, 172, 110, 167, 132, 152, 127, 135, 200]
[160, 127, 172, 200]

S3:

[3]: 1 # S 3:
2
3 import os
4
5 path = os .getcwd()
6 print(path)

S4:

[4]: 1 # S 4:
2 import platform
3
4 # OS
5 print('OS:', platform.system())
6
7 # Processor
8 print('Processor:', platform.processor())
S5:

[5]: 1 # S 5:
2 import sys
3
4 search_path = sys .path
5
6 for p in search_path:
7 print(p)

S6:

1 """
2 consonants module.
3 """
4
5 def get_consonants(text):
6
7 consonants = set()
8
9 vowels = 'aeiou'
10
11 try:
12 for letter in text:
13 if letter .lower() not in vowels and letter .isalpha():
14 consonants.add(letter)
15 except:
16 pass
17 finally:
18 return consonants

[6]: 1 # S 6:
2 import consonants
3
4 text = 'Pyton Programming Language A.... /@*-'
5 cons = consonants .get_consonants(text)
6 print(cons)

[6]: {'P', 'L', 't', 'r', 'g', 'm', 'n', 'y'}

S7:

1 """
2 vowels module.
3 """
4
5 def get_vowels(text):
6
7 vowels_set = set()
8
9 vowels = 'aeiou'
10
11 try:
12 for letter in text:
13 if letter .lower() in vowels and letter .isalpha():
14 vowels_set.add(letter)
15 except:
16 pass
17 finally:
18 return vowels_set

[7]: 1 # S 7:
2
3 import vowel
4
5 text = 'Pyton Programming Language .... /@*-'
6 vows = vowel .get_vowels(text)
7 print(vows)

[7]: {'a', 'i', 'o', 'u', 'e'}

S8:

[8]: 1 # S 8:
2 import quiz_packages
3
4 text = 'Pyton Programming Language.... /@*-'
5 cons = quiz_packages .consonants.get_consonants(text)
6 vows = quiz_packages .vowels.get_vowels(text)
7 print(cons)
8 print(vows)

[8]: {'L', 'y', 'g', 'n', 't', 'P', 'm', 'r'}


{'i', 'o', 'a', 'u', 'e'}

S9:

[9]: 1 # S 9:
2
3 # import sys
4 #
5 # for p in sys.path:
6 # print(p)
7 #
8 # ...\Python\Python39\Lib
9
10 import quiz_packages_global
cons =
11
quiz_packages_global.consonants.get_consonants(text)
12 vows = quiz_packages_global .vowels.get_vowels(text)
13 print(cons)
14 print(vows)

[9]: {'n', 't', 'r', 'y', 'L', 'g', 'P', 'm'}


{'i', 'u', 'a', 'o', 'e'}

S10:

The incorrect one is D.


In the version before 3.3 -> Python Packages must include
__init__.py.
After Python 3.3 it is not mandatory to include __init__.py.
But it is strongly recommended to include __init__.py file in
the Python packages.
5. Format Operations
This chapter is about Format Operations in Python. By
format, we mean string formatting.
In Python, String Formatting is done in 4 ways:
1. Format (%) operator (old style formatting)
2. str.format (new style formatting)
3. f-strings (String Interpolation)
4. Template Strings

We will see each one these methods with examples.


We will create a new PyCharm project for this chapter. The
project name is Format_Operations. You are recommended to
create a new project with a new virtual environment. You can also
download the project from the Github Repository of the book if
you want. Here is the project that we will develop in this chapter:
Figure 5-1: Format_Operations project for this chapter

As you see in Figure 5-1 we have no main.py file in this


project. To start with we will create our first Python file with name
_0_format_operations.py . This file will be where we call other
files as separate modules.
Format (%) Operator
The format operator, % , which is also known as “old style
formatting”, allows us to construct strings by replacing parts of the
strings with variables. In simple words, we put placeholders in the
string and % operator replaces these placeholders with some
values.
Here are the placeholders and their data types:
%s : string
%d : integer
%f : float
%.nf : float with n decimal places

To see examples of the % operator we will create a new


Python file (module) and name it as _1_format_operator.py .
And here is the first example in this file:

[1]: 1 # _1_format_operator.py file


2
3 # %s
4 # define a variable
5 day = 'Monday'
6 # use % operator
7 my_string = 'Today is %s' % day
8 print(my_string)

To be able to run this code, we have to import the


_1_format_operator.py file in _0_format_operations.py as:
import _1_format_operator. See the image below:
Figure 5-2: Importing _1_format_operator module in
_0_format_operations.py file

To run the project, right click in the code editor of


_0_format_operations.py file and select Run
‘_0_format_operations…’. And here is the output of cell 1:

[1]: Today is Monday

In cell 1, line 6, we create a string with a placeholder in it:


my_string = 'Today is %s' % day. Here %s is the placeholder
for string type. And at run time, % operator places the variable
day here. So the result is Today is Monday.
Let’s do another example. This time, let’s put a number into a
string.
[2]: 1 # %d
2 # define a numeric variable
3 num = 28
4 # use % operator in the print fn
5 print('%d is a perfect number.' % num)

[2]: 28 is a perfect number.

In cell 2, we use the % operator in the print() function as:


'%d is a perfect number.' % num. Here %d is the
placeholder and num is the variable for it. And the result is: 28
is a perfect number.

[3]: 1 # %f
2 import math
3 pi = math .pi
4 text = 'pi number in math: %f' % pi
5 print(text)

[3]: pi number in math: 3.141593

In cell 3, we first get the number pi from the math module as:
pi = math.pi. Then, we use the %f placeholder as: 'pi number
in math: %f' % pi. And the result is: pi number in math:
3.141593 .
As you see in the output of cell 3, the value of pi is 3.141593 .
Let’s say we want to display only two decimal places. This is
where we need to use %.nf placeholder.

[4]: 1 # %.nf
2 import math
3 pi = math .pi
4 text = 'pi number in math: %.2f' % pi
5 print(text)

[4]: pi number in math: 3.14

In cell 4, we use the %.2f placeholder. Here, 2 defines the


number of decimal places. And the result is: 3.14 .
Let’s use more than one placeholders in a string.

[5]: 1 # Multiple placeholders with % operator


info = 'Python released in %d and being used %.1f
2
years' % ( 1991, 30)
3 print(info)

[5]: Python released in 1991 and being used 30.0 years

In cell 5, we have two placeholders in the string which are %d


and %.1f . And we have a tuple following the % operator as
(1991, 30). Python will put the items in this tuple to the
placeholders in respective order. 1991 will be placed at %d and
30 will be at %.1f . And here is the final result: Python released
in 1991 and being used 30.0 years.
Let’s do another example. This time we will define a tuple first.
And then we will use this tuple with the % operator.

[6]: 1 # Tuple with % operator


2 info = ( 'Peter', 'Parker', 28)
3 question = "%s %s is %d years old." % info
4 print(question)

[6]: Peter Parker is 28 years old.


In cell 6, we define a tuple as: info = ('Peter', 'Parker', 28).
And in line 3, we use this tuple with % operator. Its items are
replaced to the respective placeholders in the string. And the final
result is: Peter Parker is 28 years old.
In the next example, we will use a dictionary with the %
operator . Let’s do it:

[7]: 1 # Dictionary with % operator


2 file = {
3 'path': './com/pty.py',
4 'version': 1.8,
5 'author': 'Musa Arda'
6 }
file_info = " P: %(path)s \n V: %(version).1f \n A: %
7
(author)s" % file
8 print(file_info)

[7]: P: ./com/pty.py
V: 1.8
A: Musa Arda

In cell 7, we define a dictionary named file . The keys are


‘path’ , ‘version’ and ‘author ’ . And in line 7, we use this
dictionary with the % operator. As you see, we use the dictionary
keys in the string as placeholders. For example, for the path key
we use: %(path)s . And for the version , we use: %(version).1f .
The key is in parentheses followed by the type. And \n is used as
the new line character. You can see the output in three separate
rows.
String.Format
The second string formatting method that we cover is
string.format() , or str.format() for short. It is a built-in method
on String type in Python, and it works with placeholders in form of
curly brackets: {} . The format() method formats the specified
value(s) and insert them inside the string's placeholder. And it
returns the formatted string. It’s syntax is: str.format(value_1,
value_2, ...).
We will create a new Python file for this section. The file name
is _2_str_format.py . To be able to run the codes in this file we
have to import it in _0_format_operations.py as: import
_2_str_format . You can comment out the first import which we
used in the previous section. See the image below:

Figure 5-3: Importing _2_str_format module in _0_format_operations.py


file

Let’s see some examples to understand how we use


str.format() . In the first example we will use the str.format()
method with empty placeholders. By empty we mean, there is no
indices in the curly brackets.

[8]: 1 # _2_str_format.py file


2
3 # without indexing
4 hi = 'Hi there {} {}'
5 print(hi.format('Peter', 'Parker'))

[8]: Hi there Peter Parker

In cell 8, we use empty placeholders in the string as: hi = 'Hi


there {} {}'. And in line 5, we call the format() method on this
string as: hi.format('Peter', 'Parker'). Python will replace
‘Peter ’ to the first placeholder and ‘Parker ’ to the second. So the
result is: Hi there Peter Parker.
In the next example, we will use indexed placeholders which is
more common. Let’s see:

[9]: 1 # with indexing


2 statement = 'The word {0} has {1} letters.'
3 print(statement.format('Python', len('Python')))

[9]: The word Python has 6 letters.

In cell 9, we have two placeholders in the string as {0} and {1}


which are indexed. The format() method in line 3, will fill these
placeholders with its arguments in respective order. Here is the
code: statement.format('Python', len('Python')). The text
'Python', which is the first argument, will go the first placeholder,
the one with index 0: {0} . And the value of len('Python') will fill
the second placeholder: {1} . So the result is: The word Python
has 6 letters.
We can also specify parameter names in the format() method.
And we can use these names in placeholders. Let’s see it:

[10]: 1 # parameter names


2 text = '{n} comes from {s}'
language = text .format(n='Python', s="Monty
3
Python's Flying Circus")
4 print(language)

[10]: Python comes from Monty Python's Flying Circus

In cell 10, we have named placeholders as {n} and {s} in the


text variable. These are the parameter names in the format()
method. And their values will fill the respective placeholders at run
time. As you see in the output of cell 10, {n} becomes 'Python'
and {s} becomes "Monty Python's Flying Circus" when we run
the code.
Here is another example for named placeholders:

[11]: 1 # named placeholders


2 text = '{lang} comes from {source}'
language = text .format(lang='Java', source='Java
3
Coffee type, in Java Island')
4 print(language)

[11]: Java comes from Java Coffee type, in Java Island


As you see in cell 11, the named placeholder {lang} is filled
with the text 'J a v a ' and {source} is filled with 'Java Coffee
type, in Java Island'. And you see the output of text.format()
function call with named parameters.
f-Strings
The next method for string formatting is f-strings, also known
as Literal String Interpolation. F-strings are string literals that
have an 'f' or 'F' at the beginning and curly braces containing
expressions that will be replaced with their values. Here is an
example f-string: f“Hi {user_name}”. Here, user_name is the
variable inside the string.
F-strings were introduced with Python 3.6 and are used very
commonly. They have many advantages compared to other string
formatting methods. They are faster than % operator and
str.format() and you can run Python expressions inside the f-
strings .
We will create a new Python file for this section. The file name
is _3_f_strings.py . To be able to run the codes in this file we have
to import it in _0_format_operations.py as: import
_3_f_strings . You can comment out the first two imports which
we used in the previous sections. See the image below:
Figure 5-4: Importing _3_f_strings module in _0_format_operations.py
file

Let’s start to use f-string in examples.

[12]: 1 # _3_f_strings.py file


2
3 # define two variables
4 x = 2
5 y = 3
6
7 # calculate the multiplication
8 multiplication = x * y
9
10 # print in f-string
11 print(f"{x} * {y} = {multiplication}")
[12]: 2 * 3 = 6

In cell 12, we define two variables first, x and y . Then we


calculate the multiplication of them and name the result as
multiplication . In line 11, we have an f-string which is: f"{x} *
{y} = {multiplication}". We can use variables and expressions in
the placeholders of an f-string. Here x , y and multiplication are
variables. And the result of the print statement is 2 * 3 = 6.
Let’s reconstruct the same examples in a more Pythonic way.

[13]: 1 # more Pythonic way


2 # in {} we write Python statements
3 print(f"{x} * {y} = {x * y}")

[13]: 2 * 3 = 6

In cell 13, we will do the multiplication operation directly in the


f-string placeholder as: {x * y}. This is a regular Python
expression and f-string calculates the result at run time. So the print
output is exactly the same as in cell 12.
In the last example, we will use a list inside an f-string.

[14]: 1 # list in f-string


2 info = [ 'Clark Kent', 'Metropolis', 'Daily Planet']
print(f'{ info[0] } lives in { info[1] } and works for
3
{ info[2] }')

Clark Kent lives in Metropolis and works for Daily


[14]:
Planet

In cell 14, we have a list of three elements. And inside the f-


string placeholders, we access the items of this list with indices as:
{ info[0] }, { info[1] } and { info[2] }.
As you see, f-strings are very powerful and easy to use. That’s
why they are the most common way of string formatting.
Template Strings
The last string formatting method we cover is the Template
Strings. Template Strings are built by the Template class from the
string module. Its syntax is a bit different from what we seen so
far, but it might be very helpful in some cases. The placeholders in
Template Strings are created by dollar ( $ ) symbols.
We will create a new Python file for this section. The file name
is _4_template_strings.py . To be able to run the codes in this file
we have to import it in _0_format_operations.py as: import
_4_template_strings . You can comment out the first three imports
which we used in the previous sections. See the image below:

Figure 5-5: Importing _4_template_strings module in


_0_format_operations.py file
And here is the first example for the Template Strings:

[15]: 1 # _4_template_strings.py file


2
3 from string import Template
4
5 # Ex 1
6 name = 'Bruce Wayne'
7 hero = 'Batman'
8
9 temp = Template( '$h is $n')
10 print(temp.substitute(h=hero, n=name))

[15]: Batman is Bruce Wayne

In cell 15, we import the Template class from the string


module as: from string import Template.
Then in line 9, we instantiate (create) a Template object by
calling the Template class constructor as: Template('$h is $n').
The argument we pass to the constructor is a string which has
placeholders in it. Here is the string: '$h is $n'. Where $h and $n
are placeholders. We name our newly created object as temp .
Don’t worry you will learn all about classes & objects later in
this book.
In line 10, inside the print() function, we call the substitute()
method on the temp variable as: temp.substitute(h=hero,
n=name) . We pass named parameters to the method call as
h=hero and n=name . These parameter names, h and n , are the
same names we have in the placeholders, $h and $n . And the
output is: Batman is Bruce Wayne.
As you see, the flow of Template Strings is simple.
1- You first instantiate a Template object with placeholders ( $ )
in it
2- You call the substitute method on this object with named
parameters
3- Parameter names must be the same as the placeholder names

Let’s do another example to understand it better:

[16]: 1 # Ex 2
2 num_1 = 5
3 num_2 = 8
4 result = num_1 + num_2
5
6 temp = Template( '$a + $b = $c')
7 print(temp.substitute(a=num_1, b=num_2, c=result))

[16]: 5 + 8 = 13

In cell 16, line 6, we instantiate a temp object out of the


Template class with placeholders as: temp = Template('$a + $b
= $c'). Here we have three placeholders as $a , $b and $c . Then
in line 7, we call the substitute method on this temp object and
pass the parameter values. Parameter values are the variables which
we defined earlier; num_1 , num_2 and result . And the result is:
5 + 8 = 13.
QUIZ - Format Operations
Now it’s time to solve the quiz for this chapter. You can
download the quiz file, QUIZ_Format_Operations.zip, from the
Github Repository of this book. It is in the folder named
5_Format_Operations. You should put the quiz file in the
Format_Operations project we build in this chapter.
To be able to run the codes in the Quiz file you have to import it
in _0_format_operations.py as: import QUIZ_
Format_Operations . You can comment out other imports which
we used in the previous sections.

Figure 5-6: QUIZ_Format_Operations.py file in the project

Here are the questions for this chapter:

QUIZ - Format Operations:


Q1:

Define a function named day_names.


It will ask for a day name from the user.
And will return a Tuple including the day name and number of
letters in it.
Ex: (Sunday, 6)
Call this function and get the resulting Tuple, and then print the
string below:
'Sunday has 6 letters.'

Hints:
use % operator

[1]: 1 # Q 1:
2
3 # define the function
4 # --- your solution here ---
5
6 # call the function you defined
7 # --- your solution here ---
8
9 # print with % operator
10 # --- your solution here ---

[1]: Please enter a day name: Sunday


Sunday has 6 letters.

Q2:

Call the function in Q1 and print the result with str.format() as


follows:
'day: Monday, length: 6'
Hints:
use str.format()

[2]: 1 # Q 2:
2
3 # call the day_names function
4 # --- your solution here ---
5
6 # print with str.format()
7 # --- your solution here ---

[2]: Please enter a day name: Monday


day: Monday, length: 6

Q3:

Call the function in Q1 and print the result with f-strings as


follows:
'day: Monday, length: 6'

Hints:
use f-string

[3]: 1 # Q 3:
2
3 # call the day_names function
4 # --- your solution here ---
5
6 # print with f-string
7 # --- your solution here ---

[3]: Please enter a day name: Monday


day: Monday, length: 6
Q4:

Call the function in Q1 and print the result with Template


Strings as follows:
'day: Monday, length: 6'

Hints:
use Template Strings

[4]: 1 # Q 4:
2
3 # import the necessary class
4 # --- your solution here ---
5
6 # call the day_names function
7 # --- your solution here ---
8
9 # instantiate a template object with placeholders
10 # --- your solution here ---
11
12 # print with Template String
13 # --- your solution here ---

[4]: Please enter a day name: Monday


day: Monday, length: 6

Use the dictionary below for Questions 5, 6, 7, 8, 9, 10:

capitals = {
'USA': 'Washington',
'China': 'Beijing',
'Germany': 'Berlin',
'UK': 'London'
}

Q5:

Use the capitals dictionary and % operator to print as follows:


"The capital city of USA is Washington"
'USA' will be constant, 'Washington' will be a variable.

[5]: 1 # Q 5:
2
3 # --- your solution here ---

[5]: The capital city of USA is Washington

Q6:

Use the capitals dictionary and str.format() to print as follows:


"The capital city of Germany is Berlin"
Both 'Germany' and 'Berlin' will be variables.

[6]: 1 # Q 6:
2
3 # --- your solution here ---

[6]: The capital city of Germany is Berlin

Q7:

Use the capitals dictionary and f-strings to print as follows:


"The capital city of UK is London"
Both 'UK' and 'London' will be variables.

[7]: 1 # Q 7:
2
3 # --- your solution here ---

[7]: The capital city of UK is London

Q8:

Use the capitals dictionary and Template Strings to print as


follows:
"The capital city of USA is Washington"
Both 'USA' and 'Washington' will be variables.

[8]: 1 # Q 8:
2
3 # --- your solution here ---

[8]: The capital city of USA is Washington

Q9:

Print the country names and capitals by using a for loop and f-
strings as follows:
'<capital> is the capital of <country>'

Expected Output:
Washington is the capital of USA
Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK

[9]: 1 # Q 9:
2
3 # --- your solution here ---

[9]: Washington is the capital of USA


Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK

Q10:

Print the country names and capitals by using Comprehension


and f-strings as follows:
'<capital> is the capital of <country>'

Expected Output:
Washington is the capital of USA
Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK

[10]: 1 # Q 10:
2
3 # --- your solution here ---

[10]: Washington is the capital of USA


Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK
SOLUTIONS - Format Operations
Here are the solutions for the quiz for Chapter 5 - Format
Operations.

SOLUTIONS - Format Operations:

S1:

[1]: 1 # S 1:
2
3 def day_names():
4 name = input('Please enter a day name: ')
5 return (name, len(name))
6
7 # call the function you defined
8 day, num_of_letters = day_names()
9
10 # print with % operator
11 print("%s has %d letters." % (day, num_of_letters))

[1]: Please enter a day name: Sunday


Sunday has 6 letters.

S2:

[2]: 1 # S 2:
2
3 # call the day_names function
4 day, num_of_letters = day_names()
5
6 # print with str.format()
7 print('day: {0}, lenght: {1}'.format(day,
num_of_letters))

[2]: Please enter a day name: Monday


day: Monday, lenght: 6

S3:

[3]: 1 # S 3:
2
3 # call the day_names function
4 day, num_of_letters = day_names()
5
6 # print with f-string
7 print(f"day: { day }, length: {num_of_letters}")

[3]: Please enter a day name: Monday


day: Monday, length: 6

S4:

[4]: 1 # S 4:
2
3 # import the necessary class
4 from string import Template
5
6 # call the day_names function
7 day, num_of_letters = day_names()
8
9 # print with Template String
10 template = Template( 'day: $x, length: $y')
11 print(template.substitute(x=day, y=num_of_letters))
[4]: Please enter a day name: Monday
day: Monday, length: 6

Use the dictionary below for Questions 5, 6, 7, 8, 9, 10:

capitals = {
'USA': 'Washington',
'China': 'Beijing',
'Germany': 'Berlin',
'UK': 'London'
}

S5:

[5]: 1 # S 5:
2
3 s = "The capital city of USA is %(USA)s" % capitals
4 print(s)

[5]: The capital city of USA is Washington

S6:

[6]: 1 # S 6:
2
3 country = 'Germany'
s = "The capital city of {0} is {1}".format(country,
4
capitals[country])
5 print(s)

[6]: The capital city of Germany is Berlin

S7:
[7]: 1 # S 7:
2
3 country = 'UK'
s = f "The capital city of {country} is
4
{capitals[country]}"
5 print(s)

[7]: The capital city of UK is London

S8:

[8]: 1 # S 8:
2
3 from string import Template
4
5 country = 'USA'
6 template = Template( 'The capital city of $co is $ca')
print(template.substitute(co=country,
7
ca=capitals[country]))

[8]: The capital city of USA is Washington

S9:

[9]: 1 # S 9:
2
3 for country, city in capitals .items():
4 print(f'{city} is the capital of {country}')

[9]: Washington is the capital of USA


Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK
S10:

[10]: 1 # S 10:
2
3 [
4 print(f'{city} is the capital of {country}')
5 for country, city in capitals .items()
6 ]

[10]: Washington is the capital of USA


Beijing is the capital of China
Berlin is the capital of Germany
London is the capital of UK
6. File Operations
A file is a set of bytes that store data which is organized in a
specific format.
In general, there are two types of files which are:
1. Binary Types: pdf, doc, jpg, png, mp4, zip, etc.
2. Text Types: txt, xml, html, csv, .py, .java, etc.

Encoding defines how data is stored in a file. Common


encodings types are:
1. ASCII (128 characters)
2. UNICODE (1.114.112 characters)

In this chapter we will work with files in Python. We will open,


read, write, create, delete and rename files and folders. We will
also learn how to open or create zip files in Python.
We will create a new PyCharm project for this chapter. The
project name is File_Operations. You are recommended to create
a new project with a new virtual environment. You can also
download the project from the Github Repository of the book if
you want. Here is the project that we will develop in this chapter:
Figure 6-1: File_Operations project for this chapter

As you see in Figure 6-1 we have no main.py file in this


project. To start with we will create our first Python file with name
_1_open_a_file.py . And we will need a text file named words.txt
which contains some dummy words from Lorem Ipsum. You can
find this file in the project folder of this chapter.
Opening a File
Like most of the things in Python, it is very easy to open a file.
Python has built-in open() function for this. It opens the file at the
specified path and returns the corresponding file object. Its syntax
is: open(file, mode, encoding).
The first parameter, file , is a path-like object giving the
pathname (absolute or relative to the current working directory) of
the file to be opened. For the files in the current directory, file path
is just the file name.
You can decide in which mode (read, write, create, etc.) you
want to open a file or with which encoding type. If you do not pass
any mode or encoding value to it, Python will use the default
values for them. The default value for mode is read (‘r’). And the
default value for the encoding is platform-dependent. It is better to
pass encoding when you read files, to avoid unexpected
consequences.
In the first example we will open the words.txt file without
specifying any encoding type.

[1]: 1 # _1_open_a_file.py file


2
3 # To open a file in Python -> open()
4
5 # without encoding
6 file = open('words.txt')
7 print(file.read())

[1]: Lorem
ipsum
dolor
sit
amet
. . .
. . .
Antryl
€

In cell 1, you the simplest form of the open() function. We just


pass the file path and that’s it. It returns the file object which has
the content in it. To get the entire file content we call the read()
method as: file.read() . Then we print this content and you see it in
the output.
Since we didn’t pass any encoding type, there are some
characters which the open() function couldn’t read properly. The
euro ( € ) character for example. We have this character on the last
line of words.txt file. But in the output of cell 1, we don’t see it.
Instead we see a character set of € . Which is definitely not what
we expected to see.
To fix this error, we will pass the encoding type of utf-8 .
Which is a Unicode standard and by far the most popular encoding
format in text files. Let’s do it:

[2]: 1 # without encoding -> € => €


2 # with encoding
3 file = open('words.txt', encoding='utf-8')
4 print(file.read())

[2]: Lorem
ipsum
dolor
sit
amet
. . .
. . .
Antryl

In cell 2, we pass the encoding to the open() function as:


open('words.txt', encoding='utf-8') . And in the output, we see
the correct form of the euro ( € ) character.

Important Note:
You should always close the file you open. To close the file we
use built-in file.close() method on the file object.

[3]: 1 # Important
2 # You should always close the file you open.
3 file.close()

What happens if the file which you try to open does not exist?
As you may guess, you will get an exception. The exception type
will be FileNotFoundError . Let’s see an example:

[4]: 1 # try to open a file which doesn't exist


2 file = open('wordssss.txt', encoding='utf-8')

FileNotFoundError: [Errno 2] No such file or


[4]:
directory: 'wordssss.txt'

As you see in cell 4, we call the open() function with a file


which doesn’t exist in the current project and we get
FileNotFoundError exception.
Let’s handle this exception. We will call the open() function
inside a try block and if we get an error, we will handle it in the
except block.
[5]: 1 # exception handling
2 # file doesn't exist
3 try:
4 file = open('wordssss.txt', encoding='utf-8')
5 except:
6 print('No such file in the path....')
7 else:
8 print(file.read())
9 file.close()

[5]: No such file in the path....

In cell 5, we set a try-except-else block to write a clean code


for opening a file. In the try block, we open the file in the
specified path. Since there is no file named 'wordssss.txt' in the
current directory, we get an exception. In the except block we
catch this exception and print as ‘No such file in the path....’.
Let’s re-write the same code block, but call with the correct file
name:

[6]: 1 # exception handling


2 # file exists
3 try:
4 file = open('words.txt', encoding='utf-8')
5 except:
6 print('No such file in the path....')
7 else:
8 print(file.read())
9 file.close()

[6]: Lorem
ipsum
dolor
sit
amet
. . .
. . .
Antryl

In cell 6, in the try block, we call the open() function with a


file name which exists in the current directory. So, it returns the file
object and we name it as file . Here is the code for it: file =
open('words.txt', encoding='utf-8') . Since there is no error in
the try block, the code execution will jump to the else block. This
is where we first print the file content and then close the file. See
the lines 8 and 9.

with:
In Python, we have context managers that facilitate the proper
handling of resources like opening and closing files. The with
keyword is a context manager which handles file opening and
closing operations automatically. When we use the with context
manager, we don’t need to worry about try-except-else-finally
blocks to ensure that the file is closed after usage even if there is an
exception.
Here is the sample syntax for the with context manager:

with open as file:


........
........
........
Now let’s open the words.txt file with the help of the with
context manager:

[7]: 1 # with keyword -> open and close


2 with open('words.txt', encoding='utf-8') as file:
3 content_up_to = file.read()
4 print(content_up_to)

[7]: Lorem
ipsum
dolor
sit
amet
. . .
. . .
Antryl

In cell 7, we see a common use case for the with keyword. It


uses the open() function and returns the opened file object as file .
Here is the code for it: with open('words.txt', encoding='utf-8')
as file. In the scope of the with context manager, we will use this
file object.
In line 3, we read the file content and assign it to a variable as:
content_up_to = file.read(). Then we print the content.
As soon as the scope of the with context manager ends, Python
closes the file. Which means, the file is kept open only inside of the
scope that belong to the with context manager.
We already know that, we will get an exception of type
FileNotFoundError if the file which we want to open does not
exist. It is the same for the with context manager. Let’s do an
example to see how we handle that case.
[8]: 1 # If the file doesn't exist -> FileNotFoundError
2 try:
3 with open('wordsss.txt', encoding='utf-8') as file:
4 content_up_to = file.read()
5 print(content_up_to)
6 except FileNotFoundError:
7 print('No such file in the path....')

[8]: No such file in the path....

In cell 8, we use the with context manager in a try block. And


in the except block we catch the exceptions which are of type
FileNotFoundError . If the file doesn’t exist, then we print 'No
such file in the path....' in the except block. If the file exists,
then we print its content like we did in the previous example.
So far, we only opened the files in read mode. But we have
other modes than just read. Now let’s see them:
File Opening Modes in Python:
r : Read - Opens a file for reading, error if the file does not
exist. (default value)
w : Write - Opens a file for writing, creates the file if it does
not exist.
a : Append - Opens a file for appending, creates the file if it
does not exist.
x : Create - Creates the specified file, returns an error if the file
exists.
t : Text - Text mode (default value)
b : Binary - Binary mode (e.g. images, videos, etc.)
+ : Update (read + write)

We will see each one of these in detail in the next sections. We


can use the combination of opening modes ( r, w, a, x) with file
type modes ( t, b).
For example:
‘rb’ means ‘read in binary mode’
‘rt’ means ‘read in text mode’

Let’s see two examples before ending this section.

[9]: 1 # Ex
2 # r -> read
3 # mode='r' read
4 # mode='rt' read as text
with open('words.txt', encoding='utf-8', mode='rt') as
5
file:
6 content_up_to = file.read()
7 print(content_up_to)

[9]: Lorem
ipsum
dolor
sit
amet
. . .
. . .
Antryl

In cell 9, we open the words.txt file in read and text mode as:
mode='rt' .
Let’s do another example. In this one, we will open a binary
file, an image file, in read mode. To do this we need to have the
image in the current folder. We already have such a file in the
project folder, which I prepared for you. The image is
python_logo.png . It is the official Python logo in png format.

[10]: 1 # Ex
2 # read a binary file
3 # mode='rb'
4 with open('python_logo.png', mode='rb') as file:
5 content = file.read()
6 print(content)

b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\x02\x
[10]:

In cell 10, we read a binary file in ‘rb – read binary’ mode


as: mode='rb' . And the output is an extremely long sequence of
bytes. Since the print method returns data in string format, we see
the character representation of these bytes here. To be able to print
it as an image, we need a conversion tool, which we will see later
in this book.
Reading and Writing
In this section we will cover reading and writing operations in
more detail. We will start with reading methods and do examples.
Then we will cover the writing methods.
We will create a new Python file for this section. The file name
is _2_reading_and_writing.py . You will be needing a new file to
read in this section: lorem_ipsum.txt . You can find it in the
project folder.

Reading:
We will cover three most common file reading methods in this
section. Here they are:
read(size) :
Reads some quantity of data and returns it as a string (in text
mode) or bytes object (in binary mode). size is an optional
numeric argument. When size is omitted or negative, the entire
contents of the file will be read and returned.
readline() :
Reads a single line from the file. A ‘line’ means, a set of
characters which ends with a newline character ( \n ). If it is the last
line in the file, \n can be omitted.
readlines() :
Reads all the remaining lines in the file.

[11]: 1 # Ex -> Reading


2
with open('lorem_ipsum.txt', mode='rt',
3
encoding='utf-8') as lorem:
4 content = lorem .read()
5 print(content)
Lorem ipsum dolor sit amet, consectetur adipiscing
[11]:
elit.
Etiam enim risus, volutpat ac laoreet nec, dictum in
massa.
Proin at nisl tristique, sagittis neque id, ultricies
quam.

In cell 11, we read all of the file content at once with read()
method as: lorem.read() . Here, lorem is the file object which we
get from the open() function in the with context manager. File
mode is ‘rt’ which is ‘read – text’ and the encoding is 'utf-8' .
Now let’s read only the first 50 characters in the file. This
means, we have to specify a reading size while calling the read()
method.

[12]: 1 # read the first 50 chars


with open('lorem_ipsum.txt', mode='rt',
2
encoding='utf-8') as lorem:
3 content_up_to = lorem .read(50) # read 50 chars
4 print(content_up_to)

[12]: Lorem ipsum dolor sit amet, consectetur adipiscing

In cell 12, we read only the first 50 characters in the file as:
lorem.read(50) . The value of 50 , here is the size parameter
which we pass to the read() method.
Let’s say we don’t want to read all the file content at once. We
need to read a single line instead. To do this, we need to use the
readline() method.

[13]: 1 # read the file line by line -> first line


2 with open('lorem_ipsum.txt', mode='rt',
encoding='utf-8') as lorem:
3 line_1 = lorem .readline()
4 print(line_1)

Lorem ipsum dolor sit amet, consectetur adipiscing


[13]:
elit.

In cell 13, we read a single line from the lorem file, which is
the first line. And then we print it. You can think of readline() as
a cursor that reads the current line and move to the next one and
then wait. It only reads one line and wait at the beginning of the
next one.
If you call the readline() one more time in the same with
context, it will read the second line, because it already read the
first. Let’s see it:

[14]: 1 # read the file line by line -> first two lines
with open('lorem_ipsum.txt', mode='rt',
2
encoding='utf-8') as lorem:
3 # read the first line
4 line_1 = lorem .readline()
5 print(line_1)
6 # read the second line
7 line_2 = lorem .readline()
8 print(line_2)

Lorem ipsum dolor sit amet, consectetur adipiscing


[14]:
elit.

Etiam enim risus, volutpat ac laoreet nec, dictum in


massa.
In cell 14, we see two consecutive readline() methods. The
first one reads the first line and waits. Then the second one reads
the second line in the file and waits. That’s how readline()
method works in general.
Before moving forward, let’s print the file object, lorem ,
which we get from the open() function:

[15]: 1 # print the file info


with open('lorem_ipsum.txt', mode='rt',
2
encoding='utf-8') as lorem:
3 print(lorem)

<_io.TextIOWrapper name='lorem_ipsum.txt'
[15]:
mode='rt' encoding='utf-8'>

As you see in the output of cell 15, the class name of our lorem
object is TextIOWrapper and it is in the io module of Python.
The filename is lorem_ipsum.txt , mode is ‘rt’ and encoding is
'utf-8' . The good news here is, you can loop over the content of
TextIOWrapper object. Let’s do it:

[16]: 1 # read all the lines one by one -> for loop
with open('lorem_ipsum.txt', mode='rt',
2
encoding='utf-8') as lorem:
3 for line in lorem:
4 print(line)

Lorem ipsum dolor sit amet, consectetur adipiscing


[16]:
elit.

Etiam enim risus, volutpat ac laoreet nec, dictum in


massa.
Proin at nisl tristique, sagittis neque id, ultricies
quam.

Duis laoreet turpis nisi, eu vehicula erat pretium nec.

In cell 16, we loop over the content of the lorem object. In the
for loop, we get each line one by one and then print it. You see
blank lines in the output. These lines are due to the new line
character which is \n . If you examine the content of the
lorem_ipsum.txt file you will see that each sentence is a separate
line. That means, there is a new line character at the end of each
line, even though we don’t see them.
Let’s remove the new line characters in the output now:

[17]: 1 # read all the lines one by one -> for loop
2 # get the sentences only
with open('lorem_ipsum.txt', mode='rt',
3
encoding='utf-8') as lorem:
4 for line in lorem:
5 # split(): splits the text from space chars
6 # space chars: ' ', \n, \t
7 line_content = line .split('\n')
8 # split() returns a list
9 sentence = line_content[ 0]
10 print(sentence)

Lorem ipsum dolor sit amet, consectetur adipiscing


[17]:
elit.
Etiam enim risus, volutpat ac laoreet nec, dictum
in massa.
Proin at nisl tristique, sagittis neque id, ultricies
quam.
Duis laoreet turpis nisi, eu vehicula erat pretium
nec.
In fringilla magna eget nibh porttitor, mollis mattis
neque laoreet.

In cell 17, we loop over the lorem variable. We name the


current item as line at each iteration. From the previous example,
we know that this line variable has a new line character in it. In
line 7, we call the split() method on the line , which gives us a list
of parts. Here is the code for split: line.split('\n') . We split the
line from the new line character, \n , and the first item is the
sentence itself. So we take the element at index 0 in line 9 as:
sentence = line_content[0]. Then we print the sentence.

Writing:
Like read methods we have write methods in Python. Most
commonly used write methods are as follows:
write(string) : Writes the contents of string to the file,
returning the number of characters written.
writelines() : It is used to insert multiple strings at a single
time. For a list of string elements, each string is inserted in the
text file.
To be able to write to a file in Python, we need to open it one of
the editing modes. The editing modes are:
w : Write - Opens the file in write-only mode. The pointer is
placed at the beginning of the file and this will overwrite any
existing file with the same name. So it clears all the content
and open as a blank file (DANGEROUS). It creates a new file
if one with the same name does not exist.
a : Append - Opens the file for appending new content to it
(SAFE). The pointer is placed at the end of the file. It creates a
new file if one with the same name does not exist.

You have to be very careful when you decide to open a file in


‘w’ mode. Because as soon as you call the open() function in ‘w’
mode it will overwrite any existing content in your file. Which
means you will lose your existing content.
Before doing the next example we have to back up our
lorem_ipsum.txt file, because we will lose its content in a minute.
Just copy it in the same folder with any name you like. (We have
lorem_ipsum_copy.txt in the project.) Now let’s open it in ‘w’
mode.

[18]: 1 # open with w mode


2 with open('lorem_ipsum.txt', mode='w') as lorem:
3 lorem.write('New Line from Code')

As soon as you run the code in cell 18, all the content in the
lorem_ipsum.txt file will be removed and you will see a single
line as: 'New Line from Code'. Which is the text we write in line
3, inside the with context manager. That’s why, opening files in
‘w’ mode is extremely dangerous. We write to the lorem file as:
lorem.write('New Line from Code').
Now, let’s restore the lorem_ipsum.txt file content to its
original form and try to open it in append, ‘a’ , mode:

[19]: 1 # open with a mode


2 with open('lorem_ipsum.txt', mode='a') as lorem:
3 lorem.write('New Line with append mode...')

When we run the code in cell 19, Python opens the


lorem_ipsum.txt file in append mode. Which means we don’t lose
its content and we are able to add new content to it. We call the
write() method on the lorem file as: lorem.write('New Line
with append mode...'). And it adds this sentence to the file. See
the image below:

Figure 6-2: New line appended to the lorem_ipsum.txt file

As you see, the append mode, mode='a' , is safe when you deal
with file editing. Now let’s assume that, we need to add multiple
lines at once. We will use the writelines() method for this.

[20]: 1 # append multiple lines at once


2 with open('lorem_ipsum.txt', mode='a') as lorem:
list_to_append = [ '\nMulti Line 1', '\nMulti Line 2',
3
'\nMulti Line 3']
4 lorem.writelines(list_to_append)
In cell 20, inside the with context manager, we define a list
called list_to_append . There are three elements in this list and
each element starts with a new line character, \n . For example the
first item is: '\nMulti Line 1'. That’s needed because we want to
write each element on a separate line. New line character will
provide this.
In line 4 we call the writelines() method on the lorem file
object and pass our list as the argument. Here is the code:
lorem.writelines(list_to_append) . Python adds each element in
the list as a separate line to the lorem file. See the image below:

Figure 6-3: Three new lines appended to the lorem_ipsum.txt file


Rename, Delete and Create Files
In this section we will learn how to rename, delete and create
files in Python. We will use the built-in os module for these
operations.
We will create a new Python file for this section. The file name
is _3_deleting_and_renaming.py .

Rename:
Let’s start with renaming. To rename files in Python, we use
os.rename() method. Its syntax is: os.rename(src, dst). The first
parameter, src , is the actual name of the file or directory to
rename. And the second parameter, dst , is the new name of the file
or directory.

[21]: 1 import os
2
3 # RENAME
os.rename('file_to_rename.txt',
4
'file_with_new_name.txt')

FileNotFoundError: [WinError 2] The system cannot


[21]:
find the file specified:

When we run the code in cell 21, we get an exception of type


FileNotFoundError which tell us that the system cannot find the
file specified. Because, we do not have a file in that name. As we
did before, let’s wrap this code by a try-except block to be able to
handle possible exceptions:

[22]: 1 # FileNotFoundError -> if the file not exists


2 # exception handling
3 try:
os.rename('file_to_rename.txt',
4
'file_with_new_name.txt')
5 except:
6 print('File not found with this name.')

[22]: File not found with this name.

In cell 22, we put os.rename() method call in a try-except


block. This way, our program will not crush and will keep
executing even if there is no file with the provided name.
Now, let’s create a text file with name file_to_rename.txt and
run the code in cell 22 one more time. You will see that, the
filename will change to file_with_new_name.txt .

Delete:
We will cover two ways to delete a file in Python. And we will
use the os module for both methods. These methods are:
os.remove(path)
os.unlink(path)
For both methods, if Python cannot find at the specified path, it
will raise an exception of type FileNotFoundError . So keep this
in mind, when you try to delete a file. To avoid getting errors, you
might want to check if the file exists before you try to delete it.
These methods, do not return any value if they execute
successfully.
Let’s start with remove() method. Let’s delete the empty file
which we renamed as file_with_new_name.txt :

[23]: 1 # remove a file in the current folder


2 os.remove('file_with_new_name.txt')
In cell 23, we delete the file_with_new_name.txt file as:
os.remove('file_with_new_name.txt') .
Let’s do an example of unlink() method. But we need a
dummy file to delete first. In the project folder, we will create an
empty text file with name unlink.txt . And now we can delete it:

[24]: 1 # unlink a file in the current folder


2 os.unlink('unlink.txt')

And that’s it. We delete the unlink.txt file in cell 24. As you
see, it is very easy to delete files in Python.

Create:
To be able to create a file in Python we use open() function in
create mode, which is ‘x’ . Here is the syntax: open(filename,
mode='x') . Note that, the create mode ( x ) returns an
error( FileExistsError ) if the file already exists. It does not return
any value if it creates the file successfully.

[25]: 1 # CREATE -> mode='x'


2
3 # create a file and write some content in it
4 with open('file_to_create.txt', mode='x') as new_file:
5 new_file.write('This is a new file with mode x....')

When we run the code in cell 25, Python creates a new file with
name file_to_create.txt . And then in line 5, we write the text of
'This is a new file with mode x....' in this file.
Let’s try to create a file which already exists. Since we know
we will get FileExistsError , we will do create operation in a try-
except block.
[26]: 1 # FileExistsError -> if you try to create an existing
file
2 try:
with open('file_to_create.txt', mode='x') as
3
new_file:
new_file.write('This is a new file with mode
4
x....')
5 except:
6 print('File already exist....')

[26]: File already exist....

In cell 26, in the try block, we try to create a file which already
exists in the current project. So we get FileExistsError error and
the except block is executed and it prints as 'File already
exist....' .
Getting Folder and File List
In this section we will see how can get the list of the contents in
a directory. Content types might be files and folders. We will list
them by using built-in Python modules.
We will create a new Python file for this section. The file name
is _4_getting_folder_and_file_list.py .

Get All Content (Files and Folders):


We will use three different ways to get all the content in a
directory.

Way 1: os.listdir()
os.listdir(path) returns a list containing the names of the
entries in the directory given by path. The list is in arbitrary order.
If a file is removed from or added to the directory during the call of
this function, whether a name for that file be included is
unspecified.
Let’s get all the content in the current project:

[27]: 1 # Way 1 -> os.listdir()


2 import os
3
4 # the project path
5 path = os .getcwd()
6
7 # get all the content in the project
8 content = os .listdir(path)
9 print(content)
10 for c in content:
11 print(c)
[27]: …
venv
words.txt
_1_open_a_file.py
_2_reading_and_writing.py

In cell 27, we get the current project path in line 5 as: path =
os.getcwd() . Then in line 8, we call the listdir() method with this
path as: content = os.listdir(path). This method returns a list of
content and we print this content in line 9. And finally in line 10,
we set a for loop to loop over the content and print each item one
by one.

Way 2: os.scandir()
os.scandir(path) returns an iterator of o s .D i r E n t r y objects
corresponding to the entries in the directory given by path. The
entries are yielded in arbitrary order.
Let’s get the content of the project folder with scandir()
method now:

[28]: 1 # Way 2 -> os.scandir()


2 folder = os .scandir(path)
3 for f in folder:
4 print(f)

[28]: …
<DirEntry 'venv'>
<DirEntry 'words.txt'>
<DirEntry '_1_open_a_file.py'>
<DirEntry '_2_reading_and_writing.py'>

In cell 28, we call the scandir() method with the current
project path as: folder = os.scandir(path). Then we print the
items in the folder iterator with the help of a for loop. As you see
in the output each element is of type DirEntry .

Way 3: pathlib.Path.iterdir()
In the third way, we will use the pathlib module. There is a
class called Path in this module and we will be using a method of
that class. The method name is iterdir() .

[29]: 1 # Way 3 -> pathlib.Path.iterdir()


2 from pathlib import Path
3
4 path_content = Path(path)
5
6 for inner_content in path_content .iterdir():
7 print(inner_content)

[29]: …
...\File_Operations\venv
...\File_Operations\words.txt
...\File_Operations\_1_open_a_file.py
...\File_Operations\_2_reading_and_writing.py

In cell 29, we import the Path class from the pathlib module
as: from pathlib import Path. Then in line 4 we call the Path
constructor method to create an instant of the Path class. We pass
the current project path as the argument and the result is our local
path_content variable. Then in the for loop we call the iterdir()
method of this path_content variable as:
path_content.iterdir() . We name each item as inner_content
and we print it.
The good think about the iterdir() method is it return the files
and folders with their absolute paths, which might be very useful in
certain situations.

Get All Files (Files only):


So far we get all the contents in a directory, regardless of their
types. Now we will get only the list of files. We will use the same
three ways as we did in the previous sub-section.

Way 1: os.listdir()
We will get all the content with listdir() method as we did
before. But this time, we filter only the files. We will use a built-in
method in the os.path module for filtering. The method name is
isfile() . The syntax is: os.path.isfile(content) . It returns True if
the content is a file, False otherwise. Let’s see:

[30]: 1 # Way 1 -> os.listdir()


2 # os.path.isfile()
3
4 # get all the content
5 content = os .listdir(path)
6
7 # filter the files
8 for c in content:
9 if os .path.isfile(c):
10 print(c)

[30]: …
python_logo.png
words.txt
_1_open_a_file.py
_2_reading_and_writing.py

In cell 30, we get all the content with the listdir() method as:
content = os.listdir(path). But this content includes both files
and folders in it. We have to filter only the files. Inside the for
loop, we check if the current item is a file or not as: if
os.path.isfile(c) . And we print it if it’s a file. You see a list of files
only in the output.

Way 2: os.scandir()
We will use os.scandir() again to get the content as a whole.
Then we will filter the files with is_file() method, which returns
True if the object is a file. Let’s see:

[31]: 1 # Way 2 -> os.scandir()


2 # Path.is_file()
3
4 folder = os .scandir(path)
5 for f in folder:
6 if f .is_file():
7 print(f)

[31]: …
<DirEntry 'python_logo.png'>
<DirEntry 'words.txt'>
<DirEntry '_1_open_a_file.py'>
<DirEntry '_2_reading_and_writing.py'>

In cell 31, we get all the file content in line 4 as: folder =
os.scandir(path) . And in the for loop we check if the current item
is a file or not as: if f.is_file(). And we print it if it’s a file.

Way 3: pathlib.Path.iterdir()
We will use pathlib.Path.iterdir() again to get the content as
a whole. Then we will filter the files with is_file() method. It is a
method of the Path class and returns True if the object is a file.
Let’s see:

[32]: 1 # Way 3 -> pathlib.Path.iterdir()


2 from pathlib import Path
3
4 path_content = Path(path)
5
6 for inner_content in path_content .iterdir():
7 if inner_content .is_file():
8 print(inner_content)

[32]: …
...\File_Operations\python_logo.png
...\File_Operations\words.txt
...\File_Operations\_1_open_a_file.py
...\File_Operations\_2_reading_and_writing.py

In cell 32, we import the Path class as: from pathlib import
Path . Then, we get the whole content of our project folder as:
path_content = Path(path). And we loop over the result of
path_content.iterdir() . Inside the for loop, we check if the
current item is a file or not as: if inner_content.is_file(). And we
print it if it’s a file.
Get All Folders (Folders only):
Now we are ready to get only the list of folders (directories) in a
directory. We will use the same three ways as we did in this
section.

Way 1: os.listdir()
We will use isdir() method to filter folders. The syntax is:
os.path.isdir(content) . It returns True if the content is a folder,
False otherwise. Let’s see:

[33]: 1 # Way 1 -> os.listdir()


2 # os.path.isdir()
3
4 content = os .listdir(path)
5
6 for c in content:
7 if os .path.isdir(c):
8 print(c)

[33]: …
venv
dest
example_dir_1
path_folder_1

In cell 33, inside the for loop, we filter the folders as:
os.path.isdir(c) . And in the output, you see the list of directories
in the project folder.

Way 2: os.scandir()
We will use is_dir() method to filter the directories. It is a
method of the Path class and returns True if the object is a
directory (folder). Let’s see:

[34]: 1 # Way 2 -> os.scandir()


2 folder = os .scandir(path)
3 for f in folder:
4 if f .is_dir():
5 print(f)

[34]: …
<DirEntry 'venv'>
<DirEntry 'dest'>
<DirEntry 'example_dir_1'>
<DirEntry 'path_folder_1'>

In cell 34, inside the for loop, we filter the folders as: if
f.is_dir() . The output is the list of DirEntry objects which are of
type folder in our project.

Way 3: pathlib.Path.iterdir()
To filter the directories, we will use is_dir() method from the
Path class. It returns True if the object is a directory (folder).
Let’s see:

[35]: 1 # Way 3 -> pathlib.Path.iterdir()


2 from pathlib import Path
3
4 path_content = Path(path)
5
6 for inner_content in path_content .iterdir():
7 if inner_content .is_dir():
8 print(inner_content)

[35]: …
...\File_Operations\venv
...\File_Operations\dest
...\File_Operations\example_dir_1
...\File_Operations\path_folder_1

In cell 35, in the for loop, we check each inner_content item


as: if inner_content.is_dir(). This gives True if the current item
is a directory and we print it. You see the list of all the directories
with their absolute paths.
Creating Folders
In this section we will see how we create folders (directories) in
Python. We will start by creating a single directory with os and
pathlib modules. Then we will see how to create multiple
directories.
Here are the methods that we will use:
1. os :
os.mkdir() : creates a single directory
os.makedirs() : creates multiple directories
2. pathlib :
pathlib.Path.mkdir() : creates single and multiple
directories
In each of these methods, if the file you are trying to create
already exists, then you will get an exception of type
FileExistsError . So it’s a good idea to wrap your code in a try-
except block, which we will do later. These methods does not
return any value.
We will create a new Python file for this section. The file name
is _5_creating_folders.py .

Single Directory:
Let’s start by creating a single directory with os and pathlib
modules. We will start by getting the main directory path which is
the path of our current project. In the first example we will use the
os module.

[36]: 1 # SINGLE DIRECTORY


2 import os
3 from pathlib import Path
4
5 # get the main directory path
6 main_path = os .getcwd()
7
8 # Ex
9 # Way 1 -> os.mkdir()
10
11 os.mkdir('example_dir_1')
12 os.mkdir('example_dir_2')
13 # FileExistsError -> if you create again

In cell 36, we first import os module. Then in line 6 we get the


current folder path as: main_path = os.getcwd(). This is the path
of our current project. In line 11, we create our first directory with
name example_dir_1 . And in line 12, we create a second one as:
os.mkdir('example_dir_2') . You will see these directories are
created in the project folder, when you run this code.
Now let’s create folders with the pathlib module. In cell 36,
we import the Path class from the pathlib module, which we will
be using to create directories.

[37]: 1 # Way 2 -> Path


2
3 # create a Path object
4 p = Path( 'path_folder_1')
5 # create the directory
6 p.mkdir()

In cell 37, we first create a Path object by passing the directory


name as : p = Path('path_folder_1'). Then in line 6, we call the
mkdir() method on this p object. Here is the code for this:
p.mkdir() . This statement tells Python to create a directory out of
the object p , with the name as path_folder_1 . You will see your
new directory when you run the code in cell 37.
We know that, we will get a FileExistsError if we want to
create a directory which already exists. Let’s see it with an
example:

[38]: 1 # FileExistsError
2 try:
3 p = Path( 'path_folder_1')
4 p.mkdir()
5 except FileExistsError as file_error:
6 # print('This folder exists.')
7 print(file_error)

[WinError 183] Cannot create a file when that file


[38]:
already exists: 'path_folder_1'

In cell 38, we wrap the code which creates the directory named
path_folder_1 in try block. And in the except block we catch
the exceptions of type FileExistsError . We get the standard error
description as file_error and print it, which you see in the output.
The reason for the error is that, there is already a directory with this
name in our project.

Multiple Directories:
By multiple directories, we mean nested directories. Now we
will see how we can create nested directories with a single line of
code in Python. You can also call nested directories as a folder
tree.

[39]: 1 # Way 1 -> os.makedirs()


2
3 # create a folder tree
4 os.makedirs('level 1/level 2/level 3')

In cell 39, we create nested directories with a single method


call. Here is the code: os.makedirs('level 1/level 2/level 3').
Here the makedirs() method accepts a string of nested folder
structure which is 'level 1/level 2/level 3'. It first creates the
parent folder which is level 1, then it creates the sub folder level
2 , and finally the inner most folder as level 3. See the image
below:

Figure 6-4: Nested folders created with os.makedirs()

exist_ok=True :
We saw that, we get FileExistsError when we try to create an
existing folder. And we set a try-except block to handle this case.
But there is a much more elegant way for this operation. There is a
parameter of makedirs() method which solves this problem. The
parameter name is exist_ok , and if you pass True to it
( exist_ok=True ), Python will not raise an exception when the file
exists.

[40]: 1 # FileExistsError -> to overcome -> exist_ok


2 os.makedirs('level 1/level 2/level 3', exist_ok=True)

In cell 40, we call the os.makedirs() method, with the same


folder tree which we created before. Since we pass the parameter
exist_ok=True , it doesn’t raise an exception. It just don’t do
anything. No errors, no files created. So you should use this
parameter when you are not sure whether the file already exists or
not.

In the second way of creating nested multiple directories we


will see pathlib.Path.mkdir() method.

[41]: 1 # Way 2 -> pathlib.Path.mkdir()


p2 =
2
Path('path_level_1/path_level_2/path_level_3/path_level_4'
3 # parents=True -> create all the parents
4 p2.mkdir(parents=True)

In cell 41, we instantiate a Path object with the name p2 as:


p2 =
Path('path_level_1/path_level_2/path_level_3/path_level_4').
Here, the parameter is a folder tree. Which means we expect
Python to create nested folders. But we have to declare it explicitly.
In line 4, we pass a parameter to the mkdir() method as:
parents=True . This means that we want Python to create the all
the parents in the folder tree, if they do not exist. Here is the code:
p2.mkdir(parents=True) . You will see, this folder tree is created
when you run the code in cell 41.
As the final example in this section, let’s say we want to call the
p2.mkdir() method. But we don’t want to get an error if any of the
folders in the folder tree already exist. The solution is easy. We use
exist_ok=True parameter. Here it is:

[42]: 1 # try to create again without any errors


2 p2.mkdir(parents=True, exist_ok=True)
Searching Files and Folders
In this section we will see how we search for files and folders in
a directory. We will use two different ways to do the search:
1. String Methods: startswith, endswith, find, etc.
2. fnmatch Module (recommended)
We will create a new Python file for this section. The file name
is _6_searching_files_and_folders.py .

String Methods:
We will start with using string methods. For example, let’s say
we want to search for all the Python files ( .py ) in the current
project. To do this, we will use os and pathlib modules and
endswith() string method.

[43]: 1 import os
2 from pathlib import Path
3
4 # Ex: String Methods
5
6 # search path for the current project
7 search_path = '.'
8
9 for k in os .listdir(search_path):
10 print(k)

[43]: …
venv
python_logo.png
words.txt
_1_open_a_file.py
_2_reading_and_writing.py

In cell 43, we define a search path as ‘.’ which means the


current directory that this code runs. In other words, it is the
project folder. And in the for loop, we loop over all the content
with os.listdir(search_path) and we print them.
As you see in the output of cell 43, all the files and folders are
listed. What we want to do is to filter only the files that have .py
extensions (which are regular Python files). Let’s do it:

[44]: 1 # let's get .py files


2 with Path(search_path) as path:
3 # get content
4 for content in path .iterdir():
5 # check type is file and ends with .py
if content .is_file() and
6
content.name.endswith('.py'):
7 print(content.name)

[44]: _1_open_a_file.py
_2_reading_and_writing.py
_3_deleting_and_renaming.py
_4_getting_folder_and_file_list.py

In cell 44, we use the with context manager to get the folder at
search_path which is our project directory. Here is the code: with
Path(search_path) as path. Remember that Path(search_path)
gives us the folder or file at the search_path . We name this object
as path .
In the for loop in line 4, we get the directory content with
path.iterdir() . This gives us all the content. In line 6, we filter the
content with an if statement as: if content.is_file() and
content.name.endswith('.py') . The if statement checks two
things: First, it should be a file: content.is_file() . Second, its
name should end with .py: content.name.endswith('.py') . If
these two are both True , then this means the current content is a
file that ends with .py extension.

fnmatch Module:
There is a more elegant way in Python, if you want to filter a
directory content. It uses built-in fnmatch module which supports
wildcards search. The special characters used in shell-style
wildcards are:

Pattern Meaning
* matches everything
? matches any single character
[seq] matches any character in seq
[!seq] matches any character not in seq

Since we want all the files that end with .py extension, we need
a pattern for this. This pattern should include a * character, which
means “everything that matches”.
fnmatch.fnmatch(filename, pattern): This method is used to
test whether the filename string matches the pattern string,
returning True or False .

[45]: 1 # fnmatch module (recommended)


2 import fnmatch
3
4 # match pattern with * char
5 pattern = '*.py'
6
7 with Path(search_path) as folder:
8 for f in folder .iterdir():
if f .is_file() and fnmatch .fnmatch(f.name,
9
pattern):
10 print(f.name)

[45]: _1_open_a_file.py
_2_reading_and_writing.py
_3_deleting_and_renaming.py
_4_getting_folder_and_file_list.py

In cell 45, we define a pattern with the wildcard character as:


pattern = '*.py'. This means, we want any name which ends with
.py extension. And in line 7, we set a with context manager as we
did before. In line 8, we loop over the content of the folder .
In the if statement in line 9, we check if the current content ( f )
is a file and if it matches the pattern. Here is the code for pattern
matching: fnmatch.fnmatch(f.name, pattern). Here f.name is
the filename to check. And in the output, you see all the files that
matches the '*.py' pattern, which are Python files.
Deleting, Copying and Moving Folders
In this section we will learn how to delete, copy and move
folders with Python. We will be using os , pathlib and shutil
modules.
We will create a new Python file for this section. The file name
is _7_deleting_copying_moving_folders.py .

Delete Folders:
We will cover three different ways to delete folders, which are:
1. os.rmdir() : if the folder is empty
2. pathlib.Path.rmdir() : if the folder is empty
3. shutil.rmtree() : removes in any case (empty or not)
Let’s start by creating two folders to delete:

[46]: 1 # create two folder to delete


2 os.mkdir('folder_to_delete_1')
3 os.makedirs('folder_to_delete_2/sub_folder_2')

In cell 46, we create two folders in the project directory. Be


careful that, the second folder, folder_to_delete_2 , has a sub-
folder in it. Let’s delete them now:

[47]: 1 # os
2 os.rmdir('folder_to_delete_1')

In cell 47, we delete the first folder with os as:


os.rmdir('folder_to_delete_1') . We do not get any errors because
the folder is empty.
Now let’s try to delete the second one, folder_to_delete_2 ,
which has a sub-folder in it:
[48]: 1 # pathlib
# OSError: [WinError 145] The directory is not
2
empty: 'folder_to_delete_2'
3 pathlib.Path('folder_to_delete_2').rmdir()

OSError: [WinError 145] The directory is not empty:


[48]:
'folder_to_delete_2'

In cell 48, we call the rmdir() method of path.Path class and


we pass the folder name to delete. The folder object is:
pathlib.Path('folder_to_delete_2') . But as you see in the output,
we get an error of type OSError . It tells us that we cannot delete
this folder because it is not empty.
Since the second folder is not empty, we cannot delete it with
os or pathlib modules. We have to use the shutil module for this.
Here it is:

[49]: 1 # shutil
2 shutil.rmtree('folder_to_delete_2')

In cell 49, we call the rmtree() method of the shutil module


as: shutil.rmtree('folder_to_delete_2') . We pass the folder
name as the argument and it deletes the folder and all of its
contents at once.

Copy Files:
We will use the shutil module to copy files either in the same
folder or from one directory to another. Here are the methods:
1. shutil.copy(src, dst)
2. shutil.copy2(src, dst)
The both methods copy the file src (source) to the file or
directory dst (destination). copy2() is identical to copy() except
that copy2() also attempts to preserve file metadata.
To do the copy operation, we will be needing two folders:
source and destination. Which we abbreviate as ‘src’ and ‘dest’
in the project. Before moving on, please make sure that you created
these folders in the project. Inside the src folder create a file as
source_file.txt , and put some arbitrary text in it as: ‘This is the
source file in src folder ’. Now let’s copy this file.

[50]: 1 # copy the file with a new name


2 src = 'src/source_file.txt'
3 dest = 'dest/final_file.txt'
4 shutil.copy2(src, dest)

In cell 50, we copy the file from src folder to the dest folder
as: shutil.copy2(src, dest). The file name in the source folder
was source_file.txt but we rename it in the destination folder as
final_file.txt . Their contents are exactly the same because we just
copy the source file.

Copy Folders:
We will use the shutil module to copy folders. Here is the
method for this: shutil.copytree(src, dst). It copies an entire
directory tree rooted at src to a directory named dst and returns
the destination directory.
Let’s copy the src folder and rename the copy as
src_copy_with_shutil :

[51]: 1 # FOLDERS: shutil.copytree()


2 shutil.copytree('src', 'src_copy_with_shutil')

In cell 51, we copy the entire folder tree of the src folder with a
new name as src_copy_with_shutil . When you run this code, you
will see both folders have exactly the same content in the project
directory.

Move Files and Folders:


We will use the shutil module to move files and folders. Here
is the method for this: shutil.move(src, dst). It moves a file or
directory ( src ) to another location ( dst ) and return the destination.
As an example, let’s move the src_copy_with_shutil folder
under the example_dir_1 folder which we created earlier in this
chapter. Here is the code for this:

[52]: 1 # shutil.move(src, dest)


2 shutil.move('src_copy_with_shutil', 'example_dir_1')

When you run the code in cell 52, Python will move the
src_copy_with_shutil folder under the example_dir_1 folder.
Reading and Creating Archive (Zip) Files
In this section we will see how we read and create archive (zip)
files with Python. We will be using the built-in zipfile module of
Python.
We will create a new Python file for this section. The file name
is _8_reading_and_creating_archive_files.py .
Let’s print all the content in our project to start with:

[53]: 1 import os
2
3 # print the project content
4 for d in os .listdir():
5 print(d)

[53]: …
neural_style_transfer.zip

When you run the code in cell 53, you will see that there is a zip
file in the project folder. The name of this file is
neural_style_transfer.zip . You can download it from the Github
Repository of the book. We will use this file in this section but let’s
talk a little bit about neural style transfer.

Neural Style Transfer:


Neural style transfer is an optimization technique used to take
two images - a content image and a style reference image (such as
an artwork by a famous painter) - and blend them together so the
output image looks like the content image, but “painted” in the
style of the style reference image. It is a special use case of Deep
Learning.
Inside the neural_style_transfer.zip file you will see my
study on neural style transfer. I took two images:
1. The Great Wave off Kanagawa.jpg
Katsushika Hokusai - 1829
https://en.wikipedia.org/wiki/The_Great_Wave_off_Kanagawa
2. Maidens Tower.png
The beautiful Maiden's Tower in Bosphorus, Istanbul, Turkey.
I transferred the style of The Great Wave off Kanagawa onto
Maiden’s Tower with TensorFlow, which is a Python library for
Deep Learning. See the resulting image below:

Figure 6-5: Neural style transfer applied

Read the ZIP File:


Now that you know what the neural_style_transfer.zip file is
about, let’s read it with Python.
We will use zipfile.ZipFile class, which is for reading and
writing ZIP files. The syntax is: zipfile.ZipFile(file, mode='r').
The first parameter is the file which we want to handle and the
second one is the mode . The mode parameter should be 'r' to
read an existing file, 'w' to truncate and write a new file, 'a' to
append to an existing file, or 'x' to exclusively create and write a
new file. The default value for the mode is read ( ‘r ’ ).

[54]: 1 # let's open zip file


2 import zipfile
archive = zipfile .ZipFile('neural_style_transfer.zip',
3
'r')
4 print(archive)

<zipfile.ZipFile filename='neural_style_transfer.zip'
[54]:
mode='r'>

In cell 54, we open the zip file in read mode. Be careful that,
we didn’t extract it yet, we just open the file for reading its content.
The code is: archive =
zipfile.ZipFile('neural_style_transfer.zip', 'r'). And you see
the resulting file object in the output.
Now that we open our zip file, let’s see what’s in it:

[55]: 1 # let's see what's in it


2 for d in archive .filelist:
3 print(d)

[55]: …
<ZipInfo
filename='neural_style_transfer/Maidens_Tower.png'

<ZipInfo filename='neural_style_transfer/nst.png' …

In cell 55, we print the archive file content. The ZipFile class
has a constant named filelist . It stores a list of all files within the
ZIP file. The data about each file is stored as a ZipInfo object.
The filelist constant holds a list of all these objects. That is what
we print in the output of cell 55.

Extract the ZIP File:


Now let’s extract our zip file. Once we have the archive object
which we opened in cell 54, it is very easy to extract it:

[56]: 1 # extract it
2 archive.extractall()

In cell 56, we call the extractall() method on the archive


object as: archive.extractall() . And this extract the ZIP file. You
can see the extracted file in the project directory when you run this
code.

Important Note:
You should always close the file you open with Python. If you
do not use the with context manager, then you have to close it
manually. That’s what we do next:

[57]: 1 # close the file


2 archive.close()

Create a ZIP File:


We read and extracted existing ZIP files in the previous sub-
sections. Now let’s create a new ZIP file with Python. We will use
the shutil module for this. Shutil has a method called
make_archive() . And at its name suggests, it creates archive files
for us.
Its syntax is: shutil.make_archive(base_name, format,
root_dir) .
base_name is the name of the file to create, including the path,
minus any format-specific extension.
format is the archive format: one of zip, tar, gztar, bztar, or
xztar.
root_dir is the directory that will be the root directory of the
archive, all paths in the archive will be relative to it.
Let’s see with an example:

[58]: 1 # CREATE ZIP FILES


2 import shutil
3 # create an archive file
shutil.make_archive('new_archive_folder', 'zip',
4
'neural_style_transfer')

In cell 58 we create a new ZIP file out of the existing


neural_style_transfer folder. The name of the new file is
new_archive_folder and the format is zip . You will see the
new_archive_folder.zip file in the project directory when you run
the code in cell 58.
QUIZ - File Operations
Now it’s time to solve the quiz for this chapter. You can
download the quiz file, QUIZ_File_Operations.zip, from the
Github Repository of this book. It is in the folder named
6_File_Operations. You should put the quiz file in the
File_Operations project we build in this chapter.
Here are the questions for this chapter:

QUIZ - File Operations:

Q1:

Open the 'quiz_files/flower_names.txt' file in this project with


Python.
And print the flower names.

Hints:
os
read mode ('r')

[1]: 1 # Q 1:
2 path = 'quiz_files/flower_names.txt'
3
4 # --- your solution here ---

[1]: A: African Daisy


B: Bellflower
C: Coral Bells
D: Desert Rose

Q2:

Open the 'quiz_files/flower_names.txt' file in this project with


Python.
And append the name below to this file:
'Z: Zinnia elegans'

Hints:
os
append mode ('a')

[2]: 1 # Q 2:
2
3 path = 'quiz_files/flower_names.txt'
4 new_flower = 'Z: Zinnia elegans'
5
6 # --- your solution here ---

Q3:

Create a file named 'quiz_files/file_to_delete.txt'.


And add the content below into this file:
'This file will be deleted in the Quiz.'

Hints:
os
to create file -> mode='x'
encoding

[3]: 1 # Q 3:
2
3 import os
4
5 path = 'quiz_files/file_to_delete.txt'
6
7 # --- your solution here ---

<_io.TextIOWrapper
[3]: name='quiz_files/file_to_delete.txt' mode='x'
encoding='utf-8'>

Q4:

Delete the file 'quiz_files/file_to_delete.txt' you created in Q3.


Add exception handling to handle the case of not finding such a
file.

Hints:
os
try-except
to delete -> os.remove()

[4]: 1 # Q 4:
2
3 # --- your solution here ---

Q5:

Get the list of all content (files and folders) in this project
directory.
Use this methods:
1- os.listdir()
2- os.scandir()
3- pathlib.Path.iterdir()

[5]: 1 # Q 5:
2
3 import os
4 from pathlib import Path
5
6 # 1st Way:
7 # os.listdir()
8 # --- your solution here ---
9
10 # 2nd Way
11 # os.scandir()
12 # --- your solution here ---
13
14 # 3rd Way
15 # Path
16 # . -> current folder location
17 # --- your solution here ---

[5]: .idea
dest
example_dir_1
example_dir_2

Q6:

Create the folder tree below in the 'quiz_files' directory:


- quiz_files
- folder 1
- sub folder 1
- sub folder 2
- folder 2
- folder 3
- sub folder 3
Create all of them one by one with os.mkdir().

[6]: 1 # Q 6:
2
3 # --- your solution here ---
Q7:

Create the folder tree below in the 'quiz_files' directory.


This time create with the methods described in parenthesis.
- quiz_files
(os.makedirs)
- folder 10
- sub folder 10
- sub folder 20
(pathlib.Path.mkdir)
- folder 20
- folder 30
- sub folder 30
Handle 'FileExistsError' exception if the file already exists.

Hints:
os
os.makedirs()
pathlib.Path.mkdir()
exist_ok=True
parents

[7]: 1 # Q 7:
2
3 import os
4 from pathlib import Path
5
6 # (os.makedirs)
7 # --- your solution here ---
8
9 # (pathlib.Path.mkdir)
10 # --- your solution here ---

Q8:
Find all the files which has '*a*d*.py' in the file name.
Use Comprehensions for both getting the list and printing it.

Hints:
os
os.scandir()
os.getcwd()
fnmatch

[8]: 1 # Q 8:
2
3 import os
4 import fnmatch
5
6 # get the list
7 # --- your solution here ---
8
9 # print the list
10 # --- your solution here ---

[8]: _2_reading_and_writing.py
_3_deleting_and_renaming.py
_4_getting_folder_and_file_list.py

Q9:

Delete all the files and folders in the 'quiz_files' directory,


which has '1' in its name.
Use os.path.join to join the current project path (os.getcwd) and
'quiz_files'.
And make it as your search path.
Use a single Comprehension to find and delete them.
Hints:
shutil.rmtree()
os.getcwd()
os.path.join()

[9]: 1 # Q 9:
2
3 import os
4 import shutil
5 import fnmatch
6
7 # create the search path
8 # --- your solution here ---
9
10 # define the pattern
11 # --- your solution here ---
12
13 # find the files with pattern and print
14 # --- your solution here ---
15
16 # remove files and folders with pattern
17 # --- your solution here ---

[9]: ...\File_Operations\quiz_files
folder 1
folder 10

Q10:

Create an archive (zip) folder out of 'quiz_files' folder.


The name of the archive folder is going to be
'quiz_folder_archive.zip'.
Hints:
shutil.make_archive()

[10]: 1 # Q 10:
2
3 # --- your solution here ---
SOLUTIONS - File Operations
Here are the solutions for the quiz for Chapter 6 - File
Operations.

SOLUTIONS - File Operations:

S1:

[1]: 1 # S 1:
2 path = 'quiz_files/flower_names.txt'
3
4 # 1st Way:
5 with open(path) as file:
6 flowers = file.read()
7 print(flowers)
8
9 # 2nd Way
10 with open(path, mode='r') as file:
11 for flower in file:
12 # \n char -> split()
13 # print(flower.split())
14 print(flower, end='')

[1]: A: African Daisy


B: Bellflower
C: Coral Bells
D: Desert Rose

S2:
[2]: 1 # S 2:
2
3 path = 'quiz_files/flower_names.txt'
4 new_flower = 'Z: Zinnia elegans'
5
6 with open(path, mode='a') as file:
7 # create an empty line -> \n
8 file.write('\n')
9
10 # add the new flower
11 file.write(new_flower)

S3:

[3]: 1 # S 3:
2
3 import os
4
5 path = 'quiz_files/file_to_delete.txt'
6
7 with open(path, mode='x', encoding='utf-8') as file:
8 file.write('This file will be deleted in the Quiz.')
9 print(file)

<_io.TextIOWrapper
[3]: name='quiz_files/file_to_delete.txt' mode='x'
encoding='utf-8'>

S4:

[4]: 1 # S 4:
2
3 import os
4
5 path = 'quiz_files/file_to_delete.txt'
6
7 try:
8 os.remove(path)
9 except:
10 print('File not found.')

S5:

[5]: 1 # S 5:
2
3 import os
4 from pathlib import Path
5
6 # 1st Way:
7 # os.listdir()
8 for content in os .listdir():
9 print(content)
10
11 # 2nd Way
12 # os.scandir()
13 for content in os .scandir():
14 print(content.name)
15
16 # 3rd Way
17 # Path
18 # . -> current folder location
19 for content in Path( '.').iterdir():
20 print(content)

[5]: .idea
dest
example_dir_1
example_dir_2

S6:

[6]: 1 # S 6:
2
3 import os
4
5 os.mkdir('quiz_files/folder 1')
6 os.mkdir('quiz_files/folder 1/sub folder 1')
7 os.mkdir('quiz_files/folder 1/sub folder 2')
8
9 os.mkdir('quiz_files/folder 2')
10
11 os.mkdir('quiz_files/folder 3')
12 os.mkdir('quiz_files/folder 3/sub folder 3')

S7:

[7]: 1 # S 7:
2
3 import os
4 from pathlib import Path
5
6 # (os.makedirs)
os.makedirs('quiz_files/folder 10/sub folder 10',
7
exist_ok=True)
os.makedirs('quiz_files/folder 10/sub folder 20',
8
exist_ok=True)
9
10 # (pathlib.Path.mkdir)
11 Path('quiz_files/folder 20').mkdir(exist_ok=True)
Path('quiz_files/folder 30/sub folder
12
30').mkdir(parents=True, exist_ok=True)

S8:

[8]: 1 # S 8:
2
3 import os
4 import fnmatch
5
6 pattern = '*a*d*.py'
7
8 # get the list
9 files = [ file.name
10 for file in os .scandir()
if file.is_file() and fnmatch .fnmatch(file.name,
11
pattern)]
12
13 # print the list
14 [print(f) for f in files]

[8]: _2_reading_and_writing.py
_3_deleting_and_renaming.py
_4_getting_folder_and_file_list.py

S9:

[9]: 1 # S 9:
2
3 import os
4 import shutil
5 import fnmatch
6
7 # create the search path
8 project_path = os .getcwd()
9 search_path = os .path.join(project_path, 'quiz_files')
10 print(search_path)
11
12 # define the pattern
13 pattern = '*1*'
14
15 # find the files with pattern and print
16 [print(content.name)
17 for content in os .scandir(search_path)
18 if fnmatch .fnmatch(content.name, pattern)]
19
20 # remove files and folders with pattern
21 [shutil.rmtree(content)
22 for content in os .scandir(search_path)
23 if fnmatch .fnmatch(content.name, pattern)]

[9]: ...\File_Operations\quiz_files
folder 1
folder 10

S10:

[10]: 1 # S 10:
2
3 import shutil
4
shutil.make_archive('quiz_folder_archive', 'zip',
5
'quiz_files')
Project 1 - Working with PDF & CSV Files
In this project we will learn how to work with PDF and CSV
files in Python. We will read, write, edit and copy CSV files. We
will read PDF file properties and pages, extract pages from PDF,
merge and rotate PDF files. We will install and use external Python
packages and you will have the chance to practice what you learned
so far in this book.

We will create a new PyCharm project. The project name is


PDF_and_CSV. You should download the project from the Github
Repository of the book before starting to code. Because there are
some files that you will need in the project which are movies.csv ,
movies_semicolon.csv and tutorial.pdf .
Here is the project that we will develop in this chapter:

Figure 7-1: PDF_and_CSV project for this chapter


As you see in Figure 7-1 we have three Python files in this
project; main.py , csv_operations.py and pdf_operations.py .
We will import the last two files in the main.py file as separate
modules and call the functions in them. Each time we will call a
single function to be able to see a single output. Let’s start with
CSV operations.
CSV Operations
CSV (Comma Separated Values) is a plain text file used to
store tabular data, such as a spreadsheet or database. It uses specific
structuring to arrange the data it contains. In general, in a CSV file,
each line represents a data record. And each record consists of one
or more fields, separated by a separator character. This separator is
called the delimiter, and most common delimiters are; comma (,),
semi-colon (;), tab (\t) and colon (:) characters. A CSV file must be
saved with the .csv file extension.
For this section, we will create a new Python file called
csv_operations.py . This file will include all the code we write for
handling CSV files. And we will import this file in the main.py
file as: import csv_operations.
In Python, we have a built-in module for CSV operations which
is called csv . The csv module implements classes to read and write
tabular data in CSV format. The csv module’s reader and writer
objects read and write sequences. We can also read and write data
in dictionary form using the DictReader and DictWriter classes.
We will learn how to use them in this section. Let’s start:

[1]: 1 import csv


2
3 # define the file paths
4 movie_path = 'data/movies.csv'
5 movie_path_semicolon = 'data/movies_semicolon.csv'

In cell 1, we import the csv module and define two variables to


store the paths of csv files. We will work with two csv files which
are in a folder called data .
The file names are movies.csv and movies_semicolon.csv .
Both files contain the data for 250 movies which are in IMDB Top
250 Movies list, at the time of this writing. In the first file, we have
the comma (,) character as the delimiter and in the second one the
delimiter is the semi-colon (;).
A quick note: Actually in movies.csv file we have 249 rows.
We will add the last row, row 250, later on.
In both files, the first row is the header row. Header row
contains headers for each column, if you think this data as a
spreadsheet. Here are some of the column headers: Id, Title, Year,
Rated, Released, Runtime, Genre, Director, Writer, Actors, Plot,
Language, Country, Awards, Poster, imdbRating, imdbVotes,
imdbID, Type etc.

Figure 7-2: movies.csv file

Reading CSV:
We will read movies.csv and movies_semicolon.csv files
with two different delimiters. You have to make sure that the
delimiter which you read the file with is the same character that is
saved in the file. Otherwise, you will not be able to read the csv file
successfully.
csv.reader() : This is the method which we will use to read csv
files. Its syntax is: csv.reader(csv_file, delimiter, dialect,
quoting) . We will see the parameters in detail later on in this
section.
Let’s define our first function to read the movies.csv file with
the comma (,) as the delimiter:

[2]: 1 # delimiter = ,
2 def csv_read():
3 with open(movie_path, 'r') as file:
4 # first get a csv reader
5 movies = csv .reader(file, delimiter=',')
6
7 # csv.reader() returns -> iterator
8 for movie in movies:
9 # each line is a list
10 print(movie)

In cell 2, we define a function named csv_read , which has no


parameters. In our function we have the with context manager,
which opens the file at movie_path in read ( ‘r ’ ) mode. Here is
the code: with open(movie_path, 'r') as file. And we name the
opened file object as file .
Inside the scope of the with context manager, we call the
reader() method of the csv module as: movies = csv.reader(file,
delimiter=',') . The first parameter of the csv.reader() method is
the csv file to read and the second parameter is the delimiter .
The csv.reader() returns a reader object which will iterate over
lines in the given csv file. We name this reader object as movies .
In line 8, we have a for loop to iterate over the line of the
movies object. We call the current item as movie at each iteration
and we print it.
Now that we have our csv_read() function defined, let’s call it
in the main.py file. Here is the code in the main.py file:

[3]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 csv_operations.csv_read()

[3]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',

In the main.py file we import the csv_operations module


first. Then, we call the csv_read() function in this module as:
csv_operations.csv_read() . And you see the list of movies in the
output. It’s a very long list, so we only display some parts of the
first five rows.
Let’s define a new function to read the movies_semicolon.csv
file with the semi-colon (;) as the delimiter:

[4]: 1 # delimiter = ;
2 def csv_read_semicolon():
3 with open(movie_path_semicolon, 'r') as file:
4 # first get a csv reader
5 movies = csv .reader(file, delimiter=';')
6
7 # csv.reader() returns -> iterator
8 for movie in movies:
9 # each line is a list
10 print(movie)

In cell 4, we define a new function as csv_read_semicolon . It


is very similar to the csv_read() function we defined in cell 2. The
processing logic is identical for both functions. There are only two
differences between them.
The first difference is the file path in the open() function. We
pass movie_path_semicolon as the parameter. Here is the code:
with open(movie_path_semicolon, 'r') as file.
The second difference is the delimiter which we pass to the
csv.reader() method. We pass semi-colon (;) as the delimiter
because the file we will read contains this character as the
separator. Here is the code: movies = csv.reader(file,
delimiter=';') . The rest is the same as in csv_read() function.
Let’s call this second function in the main.py file:

[5]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 csv_operations.csv_read_semicolon()

[5]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',

In cell 5, we comment out line 6, where we call the csv_read()


function. In line 7, we call our new function as:
csv_operations.csv_read_semicolon() . And you see the same
output as csv_read() function.

Dialect: Dialect is a format container for reading CSV files,


whose attributes contain information for how to handle double
quotes, whitespace, delimiters, etc.
We will use a dialect object to read our movies.csv file in the
next function. Let’s define it:

[6]: 1 # read with dialect


2 def csv_read_dialect():
3
4 csv.register_dialect('normal_read',
5 delimiter=',',
6 quoting=csv.QUOTE_MINIMAL)
7
8 with open(movie_path, 'r') as file:
9 # first get a csv reader
10 movies = csv .reader(file, dialect='normal_read')
11
12 for movie in movies:
13 print(movie)

In cell 6, we define a new function as csv_read_dialect . In


this function, we call the register_dialect() method of csv
module and pass some arguments. Here is the code:
csv.register_dialect('normal_read', delimiter=',',
quoting=csv.QUOTE_MINIMAL) . The first parameter is the
name which we give to this dialect object. We name it as
'normal_read' . The second parameter is the delimiter which we
set as comma (,). And the third parameter is the quoting format.
Quoting controls when quotes should be generated by the
writer and recognized by the reader. It can take on any of the
QUOTE_* constants and defaults to QUOTE_MINIMAL.
Here are QUOTE_* constants:
csv.QUOTE_ALL : Instructs writer objects to quote all fields.
csv.QUOTE_MINIMAL : Instructs writer objects to only
quote those fields which contain special characters such as
delimiter, quote char or any of the characters in line-
terminator.
csv.QUOTE_NONNUMERIC : Instructs writer objects to
quote all non-numeric fields. Instructs the reader to convert all
non-quoted fields to type float.
csv.QUOTE_NONE : Instructs writer objects to never quote
fields.
In line 8, we open the file at movie_path in read mode, as we
did before. And in line 10, we call the csv.reader() method with
the dialect parameter. As the value to the d i a l e c t parameter, we
pass our dialect name, 'normal_read' , which we registered earlier
in the function. The rest is the same, we print the each movie one
by one.
Now let’s call this function in the main.py file:

[7]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 csv_operations.csv_read_dialect()

[7]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',

In cell 7, you see the function call in the main.py file. We


comment out the first two functions and only call the
csv_read_dialect() as: csv_operations.csv_read_dialect() . The
output is the same as before.

Important Note: Before running the reader() method on the


csv file object, it is recommended to check if reader() can operate
on this file. In other words, if it is a valid CSV file or not. One of
the ways to get data about csv files is using a Sniffer class.

Sniffer: The csv.Sniffer class is used to deduce the format of a


CSV file. The Sniffer class provides two methods:
sniff(file_content, delimiters=None): Analyze the given
file_content and return a Dialect subclass reflecting the
parameters found. If the optional delimiters parameter is
given, it is interpreted as a string containing possible valid
delimiter characters.
has_header(file_content) : Analyze the file_content text
(presumed to be in CSV format) and return True if the first
row appears to be a series of column headers.
In the next function we will use the csv.Sniffer class to find if
our movies_semicolon.csv file has a valid header and what
delimiter it uses. Let’s define it:

[8]: 1 # sniffer fn to get data about csv file


2 def csv_sniffer():
3 with open(movie_path_semicolon, 'r') as file:
4 # get file content
5 content = file.read()
6 # check if the file has a valid header
7 has_reader = csv .Sniffer().has_header(content)
8 print('CSV Has Valid Reader:', has_reader)
9
10 # check if the file has a delimiter
11 dialect = csv .Sniffer().sniff(content)
12 print('Delimiter:', dialect.delimiter)

In cell 8, we define the csv_sniffer() function to get some data


about our csv file. We first get the file content in line 5 as: content
= file.read(). Then in line 7, call the has_header() method of the
csv.Sniffer class to check if the file has a valid header. Here is the
code for it: has_reader = csv.Sniffer().has_header(content).
And we print the result.
In line 11, we get the dialect object of our file. To do this we
call the sniff() method of the csv.Sniffer class as: dialect =
csv.Sniffer().sniff(content) . As we saw earlier the dialect object
has an attribute called delimiter . And in line 12, we print this
attribute as: dialect.delimiter .
Let’s call this function in the main.py file:
[9]: 1 # main.py file
2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 csv_operations.csv_sniffer()

[9]: CSV Has Valid Reader: True


Delimiter: ;

In cell 9, we comment out the first three lines and call the
csv_operations.csv_sniffer() function. In the output, we see that
our movies_semicolon.csv file has a valid header and the
delimiter is semi-colon (;).

Writing to CSV:
So far, we covered how we read data in a csv file. Now it’s time
to see how we write into csv files. Before start editing our csv files
it is a good idea to take backup of them. In our movies.csv file
there are 249 movies. Now we will add a new row to it, the 250th
row.
csv.writer() : This is the first method which we will use to write
into csv files. Its syntax is: csv.writer(csv_file, delimiter,
dialect, quoting). It returns a writer object responsible for
converting the user’s data into delimited strings on the given file
object.
writer.writerow() : This is the second method which we will
use to write into csv files. Its syntax is: writer.writerow(row) . It
writes the row parameter to the writer ’s file object, formatted
according to the current dialect. There is also a
writer.writerows(rows) method to write multiple rows at once.

Important Note: We will open the files in append ( 'a' ) mode,


NOT IN write ( 'w' ). Remember that, we will lose all the existing
content in the file, if we open it in ‘w’ mode.

[10]: 1 # define the new row to add


movie_to_add = [ "250","Slumdog
2
Millionaire","2008","R","25 Dec 2008",
"120 min","Drama", "Danny Boyle, Loveleen
3
Tandan",
"Simon Beaufoy (screenplay), Vikas Swarup
4
(novel)",
"Dev Patel, Saurabh Shukla, Anil Kapoor, Raj
5
Zutshi",
"A Mumbai teen reflects on his upbringing in the
6
...",
7 "English, Hindi, French","UK, France, USA",
"Won 8 Oscars. Another 144 wins & 126
8
nominations.",
9 "https://images-na.ssl-images-amazon.com/...jpg"
"Internet Movie
10
Database","8.0/10","86","8.0","679,975",
11
"tt1010048","movie","N/A","N/A","N/A","N/A","N/A"
12 "N/A","N/A","N/A","N/A","N/A",
13
"http://www.rottentomatoes.com/m/slumdog_millionaire/"
14 "31 Mar 2009","$141,243,551",
15 "Fox Searchlight Pictures",
16 "http://www.../slumdogmillionaire/","True"]
17
18 def csv_write():
19 with open(movie_path, 'a', newline='') as file:
20
21 # get a csv writer
writer = csv .writer(file, delimiter=',',
22
quoting=csv.QUOTE_ALL)
23
24 # write the row as movie
25 writer.writerow(movie_to_add)

In cell 10, we define a variable as a list to store the movie which


we will add. The variable name is movie_to_add and be careful
that it is of type list.
In line 18, we define our csv_write function. We open the file
at movie_path in append mode ( ‘a’ ). Here you see another
parameter in the open() function which is newline . And we pass
an empty string to this parameter as: newline='' . Which tells
Python, not to insert newline escape characters( \n ) at the end of
the lines. Otherwise it will conflict with the csv module. Because
csv module reads the files line by line by default. So it doesn’t
need an explicit new line character ( \n ).
In line 22, we call the writer() method as: writer =
csv.writer(file, delimiter=',', quoting=csv.QUOTE_ALL). And
we get the writer object out of this method and name it as writer .
In line 25, we call the writerow() method on our writer
object. And we pass the movie_to_add list variable which
includes the row we want to append. Here is the code:
writer.writerow(movie_to_add) .
Let’s call this function in the main.py file:

[11]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 # csv_operations.csv_sniffer()
10 csv_operations.csv_write()

In cell 11, we comment out the previous function calls and we


call the csv_operations.csv_write() function. You will see that it
adds a new line, row 250, to the movies.csv file.
In the next function let’s say we want to copy a csv file. We
need two open() function calls to complete this task. The first
open() function will open the source file and the second one will
create a blank file. So we will copy (read) the content of the source
file paste (write) it to the blank file. In other words, we will read
from the first file and write to the second one. Let’s define:

[12]: 1 # two CSV files simultaneously


2 def csv_copy():
3 # new path
4 new_movie_path = 'data/movies_copy.csv'
5
6 # create the new file -> 'x'
7 open(new_movie_path, 'x')
8
9 # open both files at the same time
10 with open(movie_path, 'r') as movies, \
open(new_movie_path, 'a', newline='') as
11 movies_copy:
12 # reader from the first one
13 movies_to_copy = csv .reader(movies)
14
15 # writer for the second one
writer = csv .writer(movies_copy,
16
quoting=csv.QUOTE_ALL)
17
# loop over movies to copy one by one -> write
18
into the new file
19 for movie in movies_to_copy:
20 writer.writerow(movie)

In cell 12, you see the definition of csv_copy() function. We


first define the path for the new file we want to create. The path is:
new_movie_path = 'data/movies_copy.csv'. It will be under the
data folder and the file name will be movies_copy.csv .
In line 7, we create this new file by calling the open() function
in create ( ‘x’ ) mode as: open(new_movie_path, 'x').
In line 10, we have the with context manager to open the files.
We call the open() functions two times in the same line. The first
one is for the movie_path file in read mode: open(movie_path,
'r') as movies. And the second one is for new_movie_path file in
append mode: open(new_movie_path, 'a', newline='') as
movies_copy . The backslash( \ ) character tells Python that the
current line continues. So, it is still the same line even if we write
some part of it on the next line.
Inside the with context manager we read the source file in line
13 as: movies_to_copy = csv.reader(movies).
In line 16, we create a csv writer object on the blank file
which is movies_copy . Here is the code: writer =
csv.writer(movies_copy, quoting=csv.QUOTE_ALL). Now this
writer object is ready to write any content which we provide to it.
In line 19, we set a for loop to iterate over the source content
which is movies_to_copy . We call each line as movie . And we
write this movie to the new file by the help of the writer object.
Here it is: writer.writerow(movie) . In summary, we read the
content of the source file line by line and we write it to the new
file.
Let’s call this function in the main.py file and copy the
movies.csv file:

[13]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 # csv_operations.csv_sniffer()
10 # csv_operations.csv_write()
11 csv_operations.csv_copy()

In cell 13, we call the csv_operations.csv_copy() function in


the main.py file. And we see a new file in the data folder which
is the copy of the movies.csv file when we run this code. You may
need to refresh (reload from disk) the folder in PyCharm if you do
not see it immediately.
PDF Operations
In this section we will cover PDF operations in Python.
Operations like, how to read PDF file properties, how to read
specific pages, how to extract pages from PDF, how to merge
multiple PDF files and how to rotate a PDF file.
For this section, we will create a new Python file called
pdf_operations.py . This file will include all the code we write for
handling PDF files. And we will import this file in the main.py
file as: import pdf_operations.
To start working with PDF files, we need some external Python
packages. These packages are PyPDF2 and pdfplumber . These
packages are not built-in. So we have to install them in the virtual
environment of the current project. We will install them in
PyCharm. If you need help on how to install packages in PyCharm
please revisit the Installing Packages section in Chapter 4 –
Modules and Packages. Where you will see the necessary steps in
detail. Here is the final screen after PyCharm installs these
packages with their dependencies:
Figure 7-3: Packages installed in this chapter

Read PDF Properties:


Now that we have the necessary packages installed, we can start
working on PDF files. In the first function we will read the PDF
file properties. These are the properties like; Number of pages,
Creator, Title, Author, Producer and Creation Date. You can also
see them in Adobe Acrobat Reader under File > Properties. You
can see the document properties of tutorial.pdf file which we will
be using in this section. This file is the official Python Tutorial
which was written by the creator of Python, Guido van Rossum
itself. It is under the data folder in the project directory.
Figure 7-4: Document Properties of tutorial.pdf file

Before defining the function let’s import the necessary


packages and define the path variables first:

[14]: 1 # pdf_operations.py file


2
3 import os
4 import PyPDF2
5 import pdfplumber
6
7 # path for pdf
8 project_path = os .getcwd()
pdf_path = os .path.join(project_path, 'pdf',
9
'tutorial.pdf')

Now let’s define our read_pdf_properties function:


[15]: 1 #--- read PDF properties ---#
2 def read_pdf_properties():
3 # read pdf
4 pdf = PyPDF2 .PdfFileReader(pdf_path)
5 print(pdf_path)
6 print(pdf)
7
8 # number of pages
9 num_of_pages = pdf .getNumPages()
10 print(f'Number of pages: {num_of_pages}')
11
12 # get document info
13 doc_info = pdf .documentInfo
14
15 for key, value in doc_info .items():
16 print(f'{key}: {value}')

In cell 15, you see the definition of read_pdf_properties


function. In line 4, we instantiate an object from PdfFileReader
class in the PyPDF2 package. We pass the path to the class
constructor. Here is the code: pdf =
PyPDF2.PdfFileReader(pdf_path) . We name our
PdfFileReader object as pdf . Then we print the pdf_path and
the pdf object.
In line 9, we call the pdf.getNumPages() to get the number of
pages in our pdf file object. And we assign the result to the
num_of_pages variable as: num_of_pages =
pdf.getNumPages() . Then we print it.
In line 13, we get the document information as: doc_info =
pdf.documentInfo . The base type of the resulting doc_info is
dictionary so we can get its items.
In line 15, we get the items of doc_info as: for key, value in
doc_info.items() . Then we print the key-value pairs.
Let’s call this function in the main.py file:

[16]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 # csv_operations.csv_read()
8 # csv_operations.csv_read_semicolon()
9 # csv_operations.csv_read_dialect()
10 # csv_operations.csv_sniffer()
11 # csv_operations.csv_write()
12 # csv_operations.csv_copy()
13 pdf_operations.read_pdf_properties()

<PyPDF2.pdf.PdfFileReader object at
[16]:
0x0000026F0BB91570>
Number of pages: 149
/Creator: LaTeX with hyperref package
/Title: Python Tutorial
/Author: Guido van Rossum, and the Python
development team
/Producer: XeTeX 0.99998
/CreationDate: D:20210112133708-00'00'

In cell 16, we import our pdf_operations module in main.py


file. We comment out all the previous functions and we call
pdf_operations.read_pdf_properties() function. You see the
document properties printed in the output.
Read Text in PDF:
In the next function we will read a single page in the PDF file.
Page number will the parameter, so we can read any page. This
time we will use the pdfplumber package. Let’s see:

[17]: 1 #--- read text in PDF ---#


2 # single page
3 def pdf_read_page(page_num=0):
4 # pdfplumber -> open()
5 with pdfplumber .open(pdf_path) as pdf:
6 page = pdf .pages[page_num]
7 content = page .extract_text(x_tolerance=1)
8 print(content)

In cell 17, we define our pdf_read_page function. It has one


parameter as page_num which has 0 as its default value.
In line 5, inside the with context manager, we call the open()
method of the pdfplumber package as: with
pdfplumber.open(pdf_path) as pdf: It is very similar to the
standard open() function in Python. But it’s in under the
pdfplumber package. We name the returning object as pdf .
This p d f object has an attribute called pages , which is a list
containing one pdfplumber.Page instance per page loaded. So we
can retrieve any page with its page number as the index as: page =
pdf.pages[page_num] . Now we have the page object
corresponding to the page_num .
In line 7, we extract the text of this page object as: content =
page.extract_text(x_tolerance=1) . The extract_text() method
returns the text content in the page . The x_tolerance=1 is
parameter is needed to be able to print the space characters
properly. And we print this text content .
Let’s call this function in the main.py file and read the text on
page 2:

[18]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 # csv_operations.csv_read()
8 # csv_operations.csv_read_semicolon()
9 # csv_operations.csv_read_dialect()
10 # csv_operations.csv_sniffer()
11 # csv_operations.csv_write()
12 # csv_operations.csv_copy()
13 # pdf_operations.read_pdf_properties()
14 pdf_operations.pdf_read_page(2)

[18]: CONTENTS
1 Whetting Your Appetite 3
2 Using the Python Interpreter 5
2.1 Invoking the Interpreter . . . . . . . . . . . . 5
2.1.1 Argument Passing . . . . . . . . . . . . 6
2.1.2 Interactive Mode. . . . . . . . . . . .6
2.2 The Interpreter and Its Environment . . . . . . . .
. . . . 6
2.2.1 Source Code Encoding . . . . . . . . . . . . 6
3 An Informal Introduction to Python 9

In cell 18, we call the pdf_operations.pdf_read_page(2)


function with 2 as the page number to read. It seems that there is
the table of contents on page 2.
In the next function we will read multiple pages from a PDF
file. Function name will be pdf_read_pages and it will take two
parameters; start and end . And it will read the pages from start
to end .

[19]: 1 # multiple pages


2 def pdf_read_pages(start=0, end=1):
3 # pdfplumber -> open()
4 with pdfplumber .open(pdf_path) as pdf:
5 for i in range(start, end + 1):
6 print(f'---------- start of page {i} ----------')
7
8 page = pdf .pages[i]
9 content = page .extract_text(x_tolerance=1)
10 print(content)
11
12 print(f'---------- end of page {i} ----------')

In cell 19, we have the definition for the pdf_read_pages


function. The default values for the parameters are: start=0,
end=1 .
In line 4, we have a with context manager. And we call the
open() method of pdfplumber package as: with
pdfplumber.open(pdf_path) as pdf.
In line 5, we set a for loop on the range of: range(start, end
+ 1). This is needed to be able to read the pages one by one and
print the content.
In line 8, we get the current page from the pages list of the pdf
object as: page = pdf.pages[i]. Then in line 9, we get the text
content in the page as: content =
page.extract_text(x_tolerance=1) . And finally we print this text
content .
Let’s call this function in the main.py file and read the text on
pages from 62 to 68 . It may take some time to complete.

[20]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.pdf_read_page(2)
9 pdf_operations.pdf_read_pages(62, 68)

[20]: ---------- start of page 62 ----------


CHAPTER
EIGHT
ERRORS AND EXCEPTIONS
Until now error messages haven’t been more than
have probably
mentioned, but if you have tried out the examples you
seen some. There are (at least) two distinguishable
kinds of errors: syntax errors and exceptions.
8.1 Syntax Errors

In cell 20, we call the pdf_operations.pdf_read_pages(62,


68) . And in the output, you see the text content from page 62 to
page 68 .

Extract Pages from PDF:


In our next function we will extract pages from a PDF file. The
page number will be a numeric parameter and we will use
PdfFileReader , PdfFileWriter classes from the PyPDF2
package.

[21]: 1 #--- extract pages from PDF ---#


2
3 # Single Page Extraction
4 def extract_page(page_num=0):
5 # import classes at once
from PyPDF2 import PdfFileReader,
6
PdfFileWriter
7
8 # define a path for extracted pdf
new_pdf_path = os .path.join(project_path, 'pdf',
9
str(page_num) + '.pdf')
10
11 # get the PDF
12 pdf = PdfFileReader(pdf_path)
13
14 # get the page
15 page = pdf .getPage(page_num)
16
17 # create new pdf
18 pdf_writer = PdfFileWriter()
19 pdf_writer.addPage(page)
20
21 # now, fill the new pdf with the pdf_writer
22 # wb -> write binary
23 with open(new_pdf_path, 'wb') as result:
24 pdf_writer.write(result)

In cell 21, we define a function called extract_page . It takes


one parameter, page_num which represents the page number that
will be extracted. The default value is 0 .
In line 6, we import two classes from the PyPDF2 package as:
from PyPDF2 import PdfFileReader, PdfFileWriter.
In line 9, we define the path for the new PDF file that will be
extracted in this function. We use os.path.join() method for this.
Here it is: os.path.join(project_path, 'pdf', str(page_num) +
'.pdf') . First, it takes the project_path global variable, then 'pdf'
for the pdf folder, then the string form of the page number plus
.pdf extension as str(page_num) + '.pdf'. If the page number is
8, let’s say, this path will be: … \PDF_and_CSV\pdf\8.pdf.
In line 12, we get a pdf object by calling the PdfFileReader
class as: pdf = PdfFileReader(pdf_path). Remember that
pdf_path is the global variable we defined in cell 14, which points
to the tutorial.pdf file in the pdf folder. That is the file we read
for extraction.
In cell 15, we call the getPage() method of the pdf object as:
page = pdf.getPage(page_num). This method gives the page
whose number is passed as the argument, which is page_num in
our case. We name the returning page object as page .
In line 18, we instantiate a PdfFileWriter object as:
pdf_writer = PdfFileWriter(). This one will give us a new,
empty PDF that we can write in.
In line 19, we call the addPage() of the pdf_writer object as:
pdf_writer.addPage(page) . The addPage() method adds the
given page object to the PDF. And we pass our page object which
we read from the source document with page_num . Please be
careful that, this pdf_writer object is not saved as a regular PDF
file yet. You can think of it as a presentation of a PDF file inside
Python code. Now we will save it as a physical PDF file on the
hard drive.
In line 23, we set a with context manager, in which we call the
open() function to open our new_pdf_path in ‘wb’ mode. ‘wb’
stands for write-binary and it is used to write binary files. Since
PDF files are in binary format, not in text format, we need to open
them in ‘wb’ mode. Remember from the previous chapter that,
‘w’ mode creates a new file if one with the same name does not
exist. So the same goes for ‘wb’ too. This means that the open()
function will create a new and empty PDF file at that path. Here is
the code: with open(new_pdf_path, 'wb') as result. We name
the resulting pdf object as result .
So far in this function here’s what we have. We have a virtual
representation of a PDF file which have a single page in it and we
name it as pdf_writer . We also have a brand new PDF object that
is opened in ‘wb’ mode and ready to be filled with data and to be
saved, which we name as result .
In cell 24, we pass the result object to the write() method of
the pdf_writer object as: pdf_writer.write(result) . And that’s
it. pdf_writer will write its content to the result object which is
our PDF file that will be saved.
Now it’s time to call this function in the main.py file. We will
call it with a page number and it will extract that page from
tutorial.pdf and save it as a new PDF file. The content of the
tutorial.pdf will not change because we open it in read mode with
PdfFileReader .

[22]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 pdf_operations.extract_page(8)
In cell 22, we call the pdf_operations.extract_page(8)
function with the page number as 8 . And the function extracts this
page and saves it as a new file, 8.pdf , in the pdf folder. You may
need to reload the folder from the disk if you don’t see it after
calling the function. See the image below for the new file in our
project:

Figure 7-5: 8.pdf file in the pdf folder

In the extract_page() function we extracted a single page from


the source file. Now let’s define a new function, extract_pages ,
that is capable of extracting multiple pages. It’s signature will be as
extract_pages(start=0, end=1). We will pass start and end
numbers for the pages we want to extract.

[23]: 1 # Multiple Pages Extraction


2 def extract_pages(start=0, end=1):
3 # import at once
from PyPDF2 import PdfFileReader,
4
PdfFileWriter
5
6 # ex: 2_8.pdf
new_pdf_path = os .path.join(project_path, 'pdf',
7
str(start) + '_' + str(end) + '.pdf')
8
9 # get the PDF
10 pdf = PdfFileReader(pdf_path)
11
12 # create new pdf
13 pdf_writer = PdfFileWriter()
14
15 # add pages
16 for i in range(start, end+1):
17 # get page
18 page = pdf .getPage(i)
19
20 # add the page into pdf_writer
21 pdf_writer.addPage(page)
22
23 # now, fill the new pdf with the pdf_writer
24 # wb -> write binary
25 with open(new_pdf_path, 'wb') as result:
26 pdf_writer.write(result)

In cell 23, you see the definition of extract_pages function,


which takes two parameters as start and end . It starts by
importing PdfFileReader and PdfFileWriter classes from
PyPDF2 .
In line 7, it concatenates start and end page numbers to create
the file path. For example if start = 2 and end = 8 then the path
will be: … \PDF_and_CSV\pdf\2_8.pdf. This is going to be the
file path for the new PDF that we will create.
In line 10, we read the source PDF file as: pdf =
PdfFileReader(pdf_path) . We will use this pdf file object to
extract pages.
In line 13, we create an instance of PdfFileWriter object as:
pdf_writer = PdfFileWriter(). And we name the instance as
pdf_writer , which we will be using for writing the pages on the
hard drive.
In line 16, we have a for loop in range(start, end+1). We
will get each page on by one from the source pdf file and then add
it to the pdf_writer object. So in line 18, we get the page at the
current index, i , as: page = pdf.getPage(i). And in line 21, we
add this page to the pdf_writer as: pdf_writer.addPage(page) .
When the loop is finished we will have all the pages added to the
pdf_writer .
In line 25, we open a new PDF file in the specified path as:
with open(new_pdf_path, 'wb') as result. Since we use ‘wb’ as
the mode, it will create a blank file for us. We name this file as
result and we will fill with the pages from the pdf_writer . That’s
why, in line 26, we call the pdf_writer.write(result) method to
write the pages into the result file. And finally, as soon as the
with context manager ends, we will get our new file saved in the
project.
Now we can call this function in the main.py file. We will call
it with page numbers from 2 to 8 as:
pdf_operations.extract_pages(2,8) . And it will create a new file
in the pdf folder as: 2_8.pdf .

[24]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 pdf_operations.extract_pages(2,8)

Merge PDF Files:


In the next function we will see how to merge multiple PDF
files. The function name will be pdf_merge and its signature will
be as: pdf_merge(*args) . Since we don’t know which PDF files
to merge, we need to use the unknown parameters, *args , for this
function. We assume that, the items in this *args parameter will
be PDF files. We will be using the PdfFileMerger from PyPDF2
package. Let’s define our function:

[25]: 1 #--- merge PDF files ---#


2
3 def pdf_merge(*args):
4 # get the class
5 from PyPDF2 import PdfFileMerger
6
7 # merger object
8 merger = PdfFileMerger()
9
10 # *args
11 for arg in args:
12 merger.append(arg)
13
14 # save merger as pdf
merged_pdf_path = os .path.join(project_path,
15
'pdf', 'merged_pdf.pdf')
16
17 # now, fill the new pdf with the merger
with open(merged_pdf_path, 'wb') as
18
merged_pdf:
19 merger.write(merged_pdf)

We define the pdf_merge function in cell 25. We start by


importing the PdfFileMerger class from PyPDF2 .
In line 8, we instantiate an object from PdfFileMerger class
and name it as merger . This object will hold the PDF files that we
want to merge.
In line 11, we set a for loop on the args parameter. And we
iterate over each element in it, which we simple call arg . Since
each arg is a PDF file we will append this file to the merger as:
merger.append(arg) .
In line 15, we create a path for the final PDF file after merge
operation. We name this variable as merged_pdf_path and the
PDF file name will be merged_pdf.pdf .
In line 18, in the with context manager, we open the
merged_pdf_path file in ‘wb’ mode which will create a blank
PDF file for us. We name this file as merged_pdf .
Finally, in line 19, we write the content of merger object into
our merged_pdf file. And that’s it. We merged multiple PDF files
and save the resulting file as merged_pdf in the pdf folder.
Let’s call this function in the main.py file. We will pass two
PDF files to it, which are ‘pdf/8.pdf ’ and ‘pdf/2_8.pdf ’ . And it
will merge them and save the result. Please be careful that, these
two files must be present in the pdf folder before you call the
function.

[26]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 # pdf_operations.extract_pages(2,8)
pdf_operations.pdf_merge('pdf/8.pdf',
13
'pdf/2_8.pdf')

Rotate PDF:
In the next function we will see how we can rotate PDF files
either clockwise or counter clockwise. We will be using the
PdfFileMerger from PyPDF2 package. Let’s rotate.

[27]: 1 #--- rotate PDF files ---#


2
3 def pdf_rotate(degree):
4 """
5 Rotates the PDF file.
6 .rotateClockwise() and
7 .rotateCounterClockwise()
8 :param degree: int, must be multiples of 90
9 :return: None
10 """
11
from PyPDF2 import PdfFileReader,
12
PdfFileWriter
13
14 # read the merged_pdf
merged_pdf =
15
PdfFileReader('pdf/merged_pdf.pdf')
16
17 # create a writer
18 writer = PdfFileWriter()
19
20 # loop over the pages
21 for i in range(merged_pdf.getNumPages()):
22 # get the page
23 page = merged_pdf .getPage(i)
24
25 # rotate the page
26 page.rotateClockwise(degree)
27
28 # pass this page to writer
29 writer.addPage(page)
30
31 # now, fill the new pdf with the writer
32 with open('pdf/rotated.pdf', 'wb') as rotated:
33 writer.write(rotated)

The function name is pdf_rotate and it takes the degree of


rotation as the parameter. We assume the degree to be a multiple of
90, because the PdfFileReader only works with these degrees.
In line 16, we create an instance of PdfFileReader class by
giving it the path which points to merged_pdf.pdf file. It is the
file we created in the previous function and we will use it for
rotation. We name the reader object as merged_pdf .
In line 19, we create a writer object by calling PdfFileWriter
class. Actually we don’t call classes, we call the constructors of
classes. Even if, you hear the term of “calling a class” it actually
means “calling the constructor of a class”. Never mind, you will
learn about classes in OOP (Object Oriented Programming)
chapter.
In line 22, we have a for loop on the range of the pages in the
merged_pdf.pdf file. We get the number of pages by calling
getNumPages() method. Here is the range:
range(merged_pdf.getNumPages()) .
In line 24, we get the current page at index i as: page =
merged_pdf.getPage(i) .
In line 27, we rotate this page clockwise as:
page.rotateClockwise(degree) .
In line 30, we add the rotated page to the writer object as:
writer.addPage(page) .
In line 33, in the with context manager, we open a new file in
‘wb’ mode with name rotated.pdf in the pdf folder. Here is the
code: with open('pdf/rotated.pdf', 'wb') as rotated. We name
the new file object as rotated .
And finally in line 34, we write the content of the writer object
to the rotated file. And this gives us a new PDF file whose pages
are rotated by the given degrees.
If you call this function in the main.py file, you will see a new
file named rotated.pdf in the pdf folder. Please make sure that,
merged_pdf.pdf file is be present in the pdf folder before you
call the function.

[28]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 # pdf_operations.extract_pages(2,8)
# pdf_operations.pdf_merge('pdf/8.pdf',
13
'pdf/2_8.pdf')
14 pdf_operations.pdf_rotate(90)

And this is the end of our project on CSV and PDF files.
In the next chapter, you will have an assignment related to this
project. You will try to re-build the same project by following the
guidelines I prepared for you.
Assignment 1 - Working with PDF & CSV
Files
We finished our first project in this book which is Project 1 -
Working with PDF & CSV Files. Now it’s your turn to recreate the
same project. This chapter is the assignment on the Project 1.
In this assignment you will read, write, edit and copy CSV files.
You will read PDF file properties and pages, extract pages from
PDF, merge and rotate PDF files. You will install and use external
Python packages and will have the chance to practice what you
learned so far in this book.
To complete the assignment, follow the instructions where you
see a #TODO statement. You can find the solutions in the previous
chapter.

We will create a new PyCharm project. The project name is


Assignment_PDF_and_CSV. You should download the project
from the Github Repository of the book before starting to code.
Because are there some files that you will need in the project which
are movies.csv , movies_semicolon.csv and tutorial.pdf .

Here is the project that we will develop in this chapter:


Figure 8-1: Assignment_PDF_and_CSV project for this chapter

As you see in Figure 8-1 we have three Python files in this


project; main.py , csv_operations.py and pdf_operations.py .
We will import the last two files in the main.py file as separate
modules and call the functions in them. Each time we will call a
single function to be able to see a single output. Let’s start with
CSV operations.
CSV Operations
CSV (Comma Separated Values) is a plain text file used to
store tabular data, such as a spreadsheet or database. It uses specific
structuring to arrange the data it contains. In general, in a CSV file,
each line represents a data record. And each record consists of one
or more fields, separated by a separator character. This separator is
called the delimiter, and most common delimiters are; comma (,),
semi-colon (;), tab (\t) and colon (:) characters. A CSV file must be
saved with the .csv file extension.
For this section, we will create a new Python file called
csv_operations.py . This file will include all the code we write for
handling CSV files. And we will import this file in the main.py
file as: import csv_operations.
In Python, we have a built-in module for CSV operations which
is called csv . The csv module implements classes to read and write
tabular data in CSV format. The csv module’s reader and writer
objects read and write sequences. We can also read and write data
in dictionary form using the DictReader and DictWriter classes.
We will learn how to use them in this section. Let’s start:

[1]: 1 import csv


2
3 # define the file paths
4 movie_path = 'data/movies.csv'
5 movie_path_semicolon = 'data/movies_semicolon.csv'

In cell 1, we import the csv module and define two variables to


store the paths of csv files. We will work with two csv files which
are in a folder called data .
The file names are movies.csv and movies_semicolon.csv .
Both files contain the data for 250 movies which are in IMDB Top
250 Movies list, at the time of this writing. In the first file, we have
the comma (,) character as the delimiter and in the second one the
delimiter is the semi-colon (;).
A quick note: Actually in movies.csv file we have 249 rows.
We will add the last row, row 250, later on.

In both files, the first row is the header row. Header row
contains headers for each column, if you think this data as a
spreadsheet. Here are some of the column headers: Id, Title, Year,
Rated, Released, Runtime, Genre, Director, Writer, Actors, Plot,
Language, Country, Awards, Poster, imdbRating, imdbVotes,
imdbID, Type etc.

Figure 8-2: movies.csv file

Reading CSV:
We will read movies.csv and movies_semicolon.csv files
with two different delimiters. You have to make sure that the
delimiter which you read the file with is the same character that is
saved in the file. Otherwise, you will not be able to read the csv file
successfully.
csv.reader() : This is the method which we will use to read csv
files. Its syntax is: csv.reader(csv_file, delimiter, dialect,
quoting) . We will see the parameters in detail later on in this
section.
Let’s define our first function to read the movies.csv file with
the comma (,) as the delimiter:

[2]: 1 # delimiter = ,
2 def csv_read():
3 with open(movie_path, 'r') as file:
4 # first get a csv reader
5 movies = csv .reader(file, delimiter=',')
6
7 # csv.reader() returns -> iterator
8 #TODO loop over movies:
9 # each line is a list
10 print(movie)

In cell 2, we define a function named csv_read , which has no


parameters. In our function we have the with context manager,
which opens the file at movie_path in read ( ‘r ’ ) mode. Here is
the code: with open(movie_path, 'r') as file. And we name the
opened file object as file .
Inside the scope of the with context manager, we call the
reader() method of the csv module as: movies = csv.reader(file,
delimiter=',') . The first parameter of the csv.reader() method is
the csv file to read and the second parameter is the delimiter .
The csv.reader() returns a reader object which will iterate over
lines in the given csv file. We name this reader object as movies .
In line 8, we have a for loop to iterate over the line of the
movies object. We call the current item as movie at each iteration
and we print it.
Now that we have our csv_read() function defined, let’s call it
in the main.py file. Here is the code in the main.py file:

[3]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 csv_operations.csv_read()

[3]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',

In the main.py file we import the csv_operations module


first. Then, we call the csv_read() function in this module as:
csv_operations.csv_read() . And you see the list of movies in the
output. It’s a very long list, so we only display some parts of the
first five rows.
Let’s define a new function to read the movies_semicolon.csv
file with the semi-colon (;) as the delimiter:

[4]: 1 # delimiter = ;
2 def csv_read_semicolon():
3 with open(movie_path_semicolon, 'r') as file:
4 # first get a csv reader
5 movies = #TODO call csv.reader() with delimiter
as ';'
6
7 # csv.reader() returns -> iterator
8 #TODO loop over movies and print

In cell 4, we define a new function as csv_read_semicolon . It


is very similar to the csv_read() function we defined in cell 2. The
processing logic is identical for both functions. There are only two
differences between them.
The first difference is the file path in the open() function. We
pass movie_path_semicolon as the parameter. Here is the code:
with open(movie_path_semicolon, 'r') as file.
The second difference is the delimiter which we pass to the
csv.reader() method. We pass semi-colon (;) as the delimiter
because the file we will read contains this character as the
separator. Here is the code: movies = csv.reader(file,
delimiter=';') . The rest is the same as in csv_read() function.
Let’s call this second function in the main.py file:

[5]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 csv_operations.csv_read_semicolon()

[5]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',



In cell 5, we comment out line 6, where we call the csv_read()


function. In line 7, we call our new function as:
csv_operations.csv_read_semicolon() . And you see the same
output as csv_read() function.

Dialect: Dialect is a format container for reading CSV files,


whose attributes contain information for how to handle double
quotes, whitespace, delimiters, etc.
We will use a dialect object to read our movies.csv file in the
next function. Let’s define it:

[6]: 1 # read with dialect


2 def csv_read_dialect():
3
4 csv.register_dialect('normal_read',
5 delimiter=',',
6 quoting=csv.QUOTE_MINIMAL)
7
8 #TODO open movie_path in read mode:
9 # first get a csv reader
10 movies = #TODO call csv.reader() with dialect
11
12 #TODO loop over movies and print

In cell 6, we define a new function as csv_read_dialect . In


this function, we call the register_dialect() method of csv
module and pass some arguments. Here is the code:
csv.register_dialect('normal_read', delimiter=',',
quoting=csv.QUOTE_MINIMAL) . The first parameter is the
name which we give to this dialect object. We name it as
'normal_read' . The second parameter is the delimiter which we
set as comma (,). And the third parameter is the quoting format.
Quoting controls when quotes should be generated by the
writer and recognized by the reader. It can take on any of the
QUOTE_* constants and defaults to QUOTE_MINIMAL.
Here are QUOTE_* constants:
csv.QUOTE_ALL : Instructs writer objects to quote all fields.
csv.QUOTE_MINIMAL : Instructs writer objects to only
quote those fields which contain special characters such as
delimiter, quote char or any of the characters in line-
terminator.
csv.QUOTE_NONNUMERIC : Instructs writer objects to
quote all non-numeric fields. Instructs the reader to convert all
non-quoted fields to type float.
csv.QUOTE_NONE : Instructs writer objects to never quote
fields.
In line 8, we open the file at movie_path in read mode, as we
did before. And in line 10, we call the csv.reader() method with
the dialect parameter. As the value to the d i a l e c t parameter, we
pass our dialect name, 'normal_read' , which we registered earlier
in the function. The rest is the same, we print the each movie one
by one.
Now let’s call this function in the main.py file:

[7]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 csv_operations.csv_read_dialect()

[7]: ['Id', 'Title', 'Year', 'Rated', 'Released', …


['1', 'The Shawshank Redemption', '1994', …
['2', 'The Godfather', '1972', 'R', '24 Mar 1972', …
['3', 'The Godfather: Part II', '1974', 'R', '20 Dec 1974',

['4', 'The Dark Knight', '2008', 'PG-13', '18 Jul 2008',

In cell 7, you see the function call in the main.py file. We


comment out the first two functions and only call the
csv_read_dialect() as: csv_operations.csv_read_dialect() . The
output is the same as before.

Important Note: Before running the reader() method on the


csv file object, it is recommended to check if reader() can operate
on this file. In other words, if it is a valid CSV file or not. One of
the ways to get data about csv files is using a Sniffer class.

Sniffer: The csv.Sniffer class is used to deduce the format of a


CSV file. The Sniffer class provides two methods:
sniff(file_content, delimiters=None): Analyze the given
file_content and return a Dialect subclass reflecting the
parameters found. If the optional delimiters parameter is
given, it is interpreted as a string containing possible valid
delimiter characters.
has_header(file_content) : Analyze the file_content text
(presumed to be in CSV format) and return True if the first
row appears to be a series of column headers.
In the next function we will use the csv.Sniffer class to find if
our movies_semicolon.csv file has a valid header and what
delimiter it uses. Let’s define it:

[8]: 1 # sniffer fn to get data about csv file


2 def csv_sniffer():
3 with open(movie_path_semicolon, 'r') as file:
4 #TODO read file content
5
#TODO use Sniffer class in csv to see if the
6
content has valid header (has_header)
7
8 print('CSV Has Valid Reader:', has_reader)
9
dialect = #TODO call sniff method of Sniffer
10
class in csv module
11 print('Delimiter:', dialect.delimiter)

In cell 8, we define the csv_sniffer() function to get some data


about our csv file. We first get the file content in line 5 as: content
= file.read(). Then in line 7, call the has_header() method of the
csv.Sniffer class to check if the file has a valid header. Here is the
code for it: has_reader = csv.Sniffer().has_header(content).
And we print the result.
In line 11, we get the dialect object of our file. To do this we
call the sniff() method of the csv.Sniffer class as: dialect =
csv.Sniffer().sniff(content) . As we saw earlier the dialect object
has an attribute called delimiter . And in line 12, we print this
attribute as: dialect.delimiter .
Let’s call this function in the main.py file:
[9]: 1 # main.py file
2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 csv_operations.csv_sniffer()

[9]: CSV Has Valid Reader: True


Delimiter: ;

In cell 9, we comment out the first three lines and call the
csv_operations.csv_sniffer() function. In the output, we see that
our movies_semicolon.csv file has a valid header and the
delimiter is semi-colon (;).

Writing to CSV:
So far, we covered how we read data in a csv file. Now it’s time
to see how we write into csv files. Before start editing our csv files
it is a good idea to take backup of them. In our movies.csv file
there are 249 movies. Now we will add a new row to it, the 250th
row.
csv.writer() : This is the first method which we will use to write
into csv files. Its syntax is: csv.writer(csv_file, delimiter,
dialect, quoting). It returns a writer object responsible for
converting the user’s data into delimited strings on the given file
object.
writer.writerow() : This is the second method which we will
use to write into csv files. Its syntax is: writer.writerow(row) . It
writes the row parameter to the writer ’s file object, formatted
according to the current dialect. There is also a
writer.writerows(rows) method to write multiple rows at once.
Important Note: We will open the files in append ( 'a' ) mode,
NOT IN write ( 'w' ). Remember that, we will lose all the existing
content in the file, if we open it in ‘w’ mode.

[10]: 1 # define the new row to add


movie_to_add = [ "250","Slumdog
2
Millionaire","2008","R","25 Dec 2008",
"120 min","Drama", "Danny Boyle, Loveleen
3
Tandan",
"Simon Beaufoy (screenplay), Vikas Swarup
4
(novel)",
"Dev Patel, Saurabh Shukla, Anil Kapoor, Raj
5
Zutshi",
"A Mumbai teen reflects on his upbringing in the
6
...",
7 "English, Hindi, French","UK, France, USA",
"Won 8 Oscars. Another 144 wins & 126
8
nominations.",
9 "https://images-na.ssl-images-amazon.com/...jpg"
"Internet Movie
10
Database","8.0/10","86","8.0","679,975",
11
"tt1010048","movie","N/A","N/A","N/A","N/A","N/A"
12 "N/A","N/A","N/A","N/A","N/A",
13
"http://www.rottentomatoes.com/m/slumdog_millionaire/"
14 "31 Mar 2009","$141,243,551",
15 "Fox Searchlight Pictures",
16 "http://www.../slumdogmillionaire/","True"]
17
18 def csv_write():
#TODO open movie_path in append mode and make sure
19
you understand newline parameter:
20
21 # get a csv writer
writer = #TODO get a csv writer with appropriate
22
parameters
23
24 # write the row as movie
25 #TODO write movie_to_add with writerow

In cell 10, we define a variable as a list to store the movie which


we will add. The variable name is movie_to_add and be careful
that it is of type list.
In line 18, we define our csv_write function. We open the file
at movie_path in append mode ( ‘a’ ). Here you see another
parameter in the open() function which is newline . And we pass
an empty string to this parameter as: newline='' . Which tells
Python, not to insert newline escape characters( \n ) at the end of
the lines. Otherwise it will conflict with the csv module. Because
csv module reads the files line by line by default. So it doesn’t
need an explicit new line character ( \n ).
In line 22, we call the writer() method as: writer =
csv.writer(file, delimiter=',', quoting=csv.QUOTE_ALL). And
we get the writer object out of this method and name it as writer .
In line 25, we call the writerow() method on our writer
object. And we pass the movie_to_add list variable which
includes the row we want to append. Here is the code:
writer.writerow(movie_to_add) .
Let’s call this function in the main.py file:

[11]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 # csv_operations.csv_sniffer()
10 csv_operations.csv_write()

In cell 11, we comment out the previous function calls and we


call the csv_operations.csv_write() function. You will see that it
adds a new line, row 250, to the movies.csv file.
In the next function let’s say we want to copy a csv file. We
need two open() function calls to complete this task. The first
open() function will open the source file and the second one will
create a blank file. So we will copy (read) the content of the source
file paste (write) it to the blank file. In other words, we will read
from the first file and write to the second one. Let’s define:

[12]: 1 # two CSV files simultaneously


2 def csv_copy():
3
4 # new path
5 new_movie_path = 'data/movies_copy.csv'
6
7 # create the new file -> 'x'
8 #TODO open new_movie_path in create mode
9
10 # open both files at the same time
with #TODO open movie_path in read mode,
11
#TODO open new_movie_path in append mode:
12
13 # reader from the first one
movies_to_copy = #TODO get csv reader for
14
movies
15
16 # writer for the second one
writer = #TODO get csv writer for
17
movies_copy
18
# loop over movies to copy one by one ->
19
write into the new file
#TODO loop over movies_to_copy and write
20
each row with csv writer

In cell 12, you see the definition of csv_copy() function. We


first define the path for the new file we want to create. The path is:
new_movie_path = 'data/movies_copy.csv'. It will be under the
data folder and the file name will be movies_copy.csv .
In line 7, we create this new file by calling the open() function
in create ( ‘x’ ) mode as: open(new_movie_path, 'x').
In line 10, we have the with context manager to open the files.
We call the open() functions two times in the same line. The first
one is for the movie_path file in read mode: open(movie_path,
'r') as movies. And the second one is for new_movie_path file in
append mode: open(new_movie_path, 'a', newline='') as
movies_copy . The backslash( \ ) character tells Python that the
current line continues. So, it is still the same line even if we write
some part of it on the next line.
Inside the with context manager we read the source file in line
13 as: movies_to_copy = csv.reader(movies).
In line 16, we create a csv writer object on the blank file
which is movies_copy . Here is the code: writer =
csv.writer(movies_copy, quoting=csv.QUOTE_ALL). Now this
writer object is ready to write any content which we provide to it.
In line 19, we set a for loop to iterate over the source content
which is movies_to_copy . We call each line as movie . And we
write this movie to the new file by the help of the writer object.
Here it is: writer.writerow(movie) . In summary, we read the
content of the source file line by line and we write it to the new
file.
Let’s call this function in the main.py file and copy the
movies.csv file:

[13]: 1 # main.py file


2
3 import csv_operations
4
5 if __name__ == '__main__':
6 # csv_operations.csv_read()
7 # csv_operations.csv_read_semicolon()
8 # csv_operations.csv_read_dialect()
9 # csv_operations.csv_sniffer()
10 # csv_operations.csv_write()
11 csv_operations.csv_copy()

In cell 13, we call the csv_operations.csv_copy() function in


the main.py file. And we see a new file in the data folder which
is the copy of the movies.csv file when we run this code. You may
need to refresh (reload from disk) the folder in PyCharm if you do
not see it immediately.
PDF Operations
In this section we will cover PDF operations in Python.
Operations like, how to read PDF file properties, how to read
specific pages, how to extract pages from PDF, how to merge
multiple PDF files and how to rotate a PDF file.
For this section, we will create a new Python file called
pdf_operations.py . This file will include all the code we write for
handling PDF files. And we will import this file in the main.py
file as: import pdf_operations.
To start working with PDF files, we need some external Python
packages. These packages are PyPDF2 and pdfplumber . These
packages are not built-in. So we have to install them in the virtual
environment of the current project. We will install them in
PyCharm. If you need help on how to install packages in PyCharm
please revisit the Installing Packages section in Chapter 4 –
Modules and Packages. Where you will see the necessary steps in
detail. Here is the final screen after PyCharm installs these
packages with their dependencies:

Figure 8-3: Packages you should install for this chapter


Read PDF Properties:
Now that we have the necessary packages installed, we can start
working on PDF files. In the first function we will read the PDF
file properties. These are the properties like; Number of pages,
Creator, Title, Author, Producer and Creation Date. You can also
see them in Adobe Acrobat Reader under File > Properties. You
can see the document properties of tutorial.pdf file which we will
be using in this section. This file is the official Python Tutorial
which was written by the creator of Python, Guido van Rossum
itself. It is under the data folder in the project directory.

Figure 8-4: Document Properties of tutorial.pdf file

Before defining the function let’s import the necessary


packages and define the path variables first:
[14]: 1 # pdf_operations.py file
2
3 #TODO install PyPDF2 and pdfplumber packages
4 import os
5 import PyPDF2
6 import pdfplumber
7
8 # path for pdf
9 project_path = os .getcwd()
pdf_path = os .path.join(project_path, 'pdf',
10
'tutorial.pdf')

Now let’s define our read_pdf_properties function:

[15]: 1 #--- read PDF properties ---#


2 def read_pdf_properties():
3 # read pdf
4 pdf = PyPDF2 .PdfFileReader(pdf_path)
5 print(pdf_path)
6 print(pdf)
7
8 # number of pages
num_of_pages = #TODO call getNumPages() on
9
pdf
10 #TODO print num_of_pages with f-strings
11
12 # get document info
13 doc_info = #TODO get documentInfo of pdf
14
15 #TODO print each key-value pair doc_info items

In cell 15, you see the definition of read_pdf_properties


function. In line 4, we instantiate an object from PdfFileReader
class in the PyPDF2 package. We pass the path to the class
constructor. Here is the code: pdf =
PyPDF2.PdfFileReader(pdf_path) . We name our
PdfFileReader object as pdf . Then we print the pdf_path and
the pdf object.
In line 9, we call the pdf.getNumPages() to get the number of
pages in our pdf file object. And we assign the result to the
num_of_pages variable as: num_of_pages =
pdf.getNumPages() . Then we print it.
In line 13, we get the document information as: doc_info =
pdf.documentInfo . The base type of the resulting doc_info is
dictionary so we can get its items.
In line 15, we get the items of doc_info as: for key, value in
doc_info.items() . Then we print the key-value pairs.
Let’s call this function in the main.py file:

[16]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 # csv_operations.csv_read()
8 # csv_operations.csv_read_semicolon()
9 # csv_operations.csv_read_dialect()
10 # csv_operations.csv_sniffer()
11 # csv_operations.csv_write()
12 # csv_operations.csv_copy()
13 pdf_operations.read_pdf_properties()

<PyPDF2.pdf.PdfFileReader object at
[16]:
0x0000026F0BB91570>
Number of pages: 149
/Creator: LaTeX with hyperref package
/Title: Python Tutorial
/Author: Guido van Rossum, and the Python
development team
/Producer: XeTeX 0.99998
/CreationDate: D:20210112133708-00'00'

In cell 16, we import our pdf_operations module in main.py


file. We comment out all the previous functions and we call
pdf_operations.read_pdf_properties() function. You see the
document properties printed in the output.

Read Text in PDF:


In the next function we will read a single page in the PDF file.
Page number will the parameter, so we can read any page. This
time we will use the pdfplumber package. Let’s see:

[17]: 1 #--- read text in PDF ---#


2 # single page
3 def pdf_read_page(page_num=0):
4 # pdfplumber -> open()
5 with pdfplumber .open(pdf_path) as pdf:
page = #TODO get page from pdf.pages at
6
page_num index
content = #TODO extract page text
7
(extract_text())
8 print(content)

In cell 17, we define our pdf_read_page function. It has one


parameter as page_num which has 0 as its default value.
In line 5, inside the with context manager, we call the open()
method of the pdfplumber package as: with
pdfplumber.open(pdf_path) as pdf: It is very similar to the
standard open() function in Python. But it’s in under the
pdfplumber package. We name the returning object as pdf .
This p d f object has an attribute called pages , which is a list
containing one pdfplumber.Page instance per page loaded. So we
can retrieve any page with its page number as the index as: page =
pdf.pages[page_num] . Now we have the page object
corresponding to the page_num .
In line 7, we extract the text of this page object as: content =
page.extract_text(x_tolerance=1) . The extract_text() method
returns the text content in the page . The x_tolerance=1 is
parameter is needed to be able to print the space characters
properly. And we print this text content .
Let’s call this function in the main.py file and read the text on
page 2:

[18]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 # csv_operations.csv_read()
8 # csv_operations.csv_read_semicolon()
9 # csv_operations.csv_read_dialect()
10 # csv_operations.csv_sniffer()
11 # csv_operations.csv_write()
12 # csv_operations.csv_copy()
13 # pdf_operations.read_pdf_properties()
14 pdf_operations.pdf_read_page(2)
[18]: CONTENTS
1 Whetting Your Appetite 3
2 Using the Python Interpreter 5
2.1 Invoking the Interpreter . . . . . . . . . . . . 5
2.1.1 Argument Passing . . . . . . . . . . . . 6
2.1.2 Interactive Mode. . . . . . . . . . . .6
2.2 The Interpreter and Its Environment . . . . . . . .
. . . . 6
2.2.1 Source Code Encoding . . . . . . . . . . . . 6
3 An Informal Introduction to Python 9

In cell 18, we call the pdf_operations.pdf_read_page(2)


function with 2 as the page number to read. It seems that there is
the table of contents on page 2.
In the next function we will read multiple pages from a PDF
file. Function name will be pdf_read_pages and it will take two
parameters; start and end . And it will read the pages from start
to end .

[19]: 1 # multiple pages


2 def pdf_read_pages(start=0, end=1):
3 # pdfplumber -> open()
4 with #TODO open pdf_path with pdfplumber:
for #TODO loop over range from start to end
5
(inc):
6 print(f'---------- start of page {i} ----------')
7
8 page = #TODO get page i from pdf
content = #TODO extract page text
9
(extract_text())
10 print(content)
11
12 print(f'---------- end of page {i} ----------')

In cell 19, we have the definition for the pdf_read_pages


function. The default values for the parameters are: start=0,
end=1 .
In line 4, we have a with context manager. And we call the
open() method of pdfplumber package as: with
pdfplumber.open(pdf_path) as pdf.
In line 5, we set a for loop on the range of: range(start, end
+ 1). This is needed to be able to read the pages one by one and
print the content.
In line 8, we get the current page from the pages list of the pdf
object as: page = pdf.pages[i]. Then in line 9, we get the text
content in the page as: content =
page.extract_text(x_tolerance=1) . And finally we print this text
content .
Let’s call this function in the main.py file and read the text on
pages from 62 to 68 . It may take some time to complete.

[20]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.pdf_read_page(2)
9 pdf_operations.pdf_read_pages(62, 68)

[20]: ---------- start of page 62 ----------


CHAPTER
EIGHT
ERRORS AND EXCEPTIONS
Until now error messages haven’t been more than
have probably
mentioned, but if you have tried out the examples you
seen some. There are (at least) two distinguishable
kinds of errors: syntax errors and exceptions.
8.1 Syntax Errors

In cell 20, we call the pdf_operations.pdf_read_pages(62,


68) . And in the output, you see the text content from page 62 to
page 68 .

Extract Pages from PDF:


In our next function we will extract pages from a PDF file. The
page number will be a numeric parameter and we will use
PdfFileReader , PdfFileWriter classes from the PyPDF2
package.

[21]: 1 #--- extract pages from PDF ---#


2
3 # Single Page Extraction
4 def extract_page(page_num=0):
5
6 # import at once
#TODO import PdfFileReader, PdfFileWriter
7
from PyPDF2 package
8
new_pdf_path = os .path.join(project_path, 'pdf',
9
str(page_num) + '.pdf')
10
11 # get the PDF
12 pdf = #TODO get PdfFileReader with pdf_path
13
14 # get the page
15 page = #TODO get page at page_num of pdf
16
17 # create new pdf
pdf_writer = #TODO instantiate PdfFileWriter
18
object
19 #TODO add page to pdf_writer
20
21 # now, fill the new pdf with the pdf_writer
22 # wb -> write binary
with #TODO open new_pdf_path in write-
23
binary mode:
24 pdf_writer.write(result)

In cell 21, we define a function called extract_page . It takes


one parameter, page_num which represents the page number that
will be extracted. The default value is 0 .
In line 6, we import two classes from the PyPDF2 package as:
from PyPDF2 import PdfFileReader, PdfFileWriter.
In line 9, we define the path for the new PDF file that will be
extracted in this function. We use os.path.join() method for this.
Here it is: os.path.join(project_path, 'pdf', str(page_num) +
'.pdf') . First, it takes the project_path global variable, then 'pdf'
for the pdf folder, then the string form of the page number plus
.pdf extension as str(page_num) + '.pdf'. If the page number is
8, let’s say, this path will be: … \PDF_and_CSV\pdf\8.pdf.
In line 12, we get a pdf object by calling the PdfFileReader
class as: pdf = PdfFileReader(pdf_path). Remember that
pdf_path is the global variable we defined in cell 14, which points
to the tutorial.pdf file in the pdf folder. That is the file we read
for extraction.
In cell 15, we call the getPage() method of the pdf object as:
page = pdf.getPage(page_num). This method gives the page
whose number is passed as the argument, which is page_num in
our case. We name the returning page object as page .
In line 18, we instantiate a PdfFileWriter object as:
pdf_writer = PdfFileWriter(). This one will give us a new,
empty PDF that we can write in.
In line 19, we call the addPage() of the pdf_writer object as:
pdf_writer.addPage(page) . The addPage() method adds the
given page object to the PDF. And we pass our page object which
we read from the source document with page_num . Please be
careful that, this pdf_writer object is not saved as a regular PDF
file yet. You can think of it as a presentation of a PDF file inside
Python code. Now we will save it as a physical PDF file on the
hard drive.
In line 23, we set a with context manager, in which we call the
open() function to open our new_pdf_path in ‘wb’ mode. ‘wb’
stands for write-binary and it is used to write binary files. Since
PDF files are in binary format, not in text format, we need to open
them in ‘wb’ mode. Remember from the previous chapter that,
‘w’ mode creates a new file if one with the same name does not
exist. So the same goes for ‘wb’ too. This means that the open()
function will create a new and empty PDF file at that path. Here is
the code: with open(new_pdf_path, 'wb') as result. We name
the resulting pdf object as result .
So far in this function here’s what we have. We have a virtual
representation of a PDF file which have a single page in it and we
name it as pdf_writer . We also have a brand new PDF object that
is opened in ‘wb’ mode and ready to be filled with data and to be
saved, which we name as result .
In cell 24, we pass the result object to the write() method of
the pdf_writer object as: pdf_writer.write(result) . And that’s
it. pdf_writer will write its content to the result object which is
our PDF file that will be saved.
Now it’s time to call this function in the main.py file. We will
call it with a page number and it will extract that page from
tutorial.pdf and save it as a new PDF file. The content of the
tutorial.pdf will not change because we open it in read mode with
PdfFileReader .

[22]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 pdf_operations.extract_page(8)

In cell 22, we call the pdf_operations.extract_page(8)


function with the page number as 8 . And the function extracts this
page and saves it as a new file, 8.pdf , in the pdf folder. You may
need to reload the folder from the disk if you don’t see it after
calling the function. See the image below for the new file in our
project:
Figure 8-5: 8.pdf file in the pdf folder

In the extract_page() function we extracted a single page from


the source file. Now let’s define a new function, extract_pages ,
that is capable of extracting multiple pages. It’s signature will be as
extract_pages(start=0, end=1). We will pass start and end
numbers for the pages we want to extract.

[23]: 1 # Multiple Pages Extraction


2 def extract_pages(start=0, end=1):
3
4 # import at once
#TODO import PdfFileReader, PdfFileWriter
5
from PyPDF2 package
6
7 # ex: 2_8.pdf
new_pdf_path = os .path.join(project_path, 'pdf',
8
str(start) + '_' + str(end) + '.pdf')
9
10 # get the PDF
11 pdf = #TODO get PdfFileReader with pdf_path
12
13 # create new pdf
pdf_writer = #TODO instantiate PdfFileWriter
14
object
15
16 # add pages
for #TODO loop over range from start to end
17
(inc):
18 # get page
19 page = #TODO get page i from pdf
20
21 # add the page into pdf_writer
22 #TODO add this page to pdf_writer
23
24 # now, fill the new pdf with the pdf_writer
25 # wb -> write binary
with #TODO open new_pdf_path in write-binary
26
mode:
#TODO write the pdf_writer into
27
new_pdf_path object

In cell 23, you see the definition of extract_pages function,


which takes two parameters as start and end . It starts by
importing PdfFileReader and PdfFileWriter classes from
PyPDF2 .
In line 7, it concatenates start and end page numbers to create
the file path. For example if start = 2 and end = 8 then the path
will be: … \PDF_and_CSV\pdf\2_8.pdf. This is going to be the
file path for the new PDF that we will create.
In line 10, we read the source PDF file as: pdf =
PdfFileReader(pdf_path) . We will use this pdf file object to
extract pages.
In line 13, we create an instance of PdfFileWriter object as:
pdf_writer = PdfFileWriter(). And we name the instance as
pdf_writer , which we will be using for writing the pages on the
hard drive.
In line 16, we have a for loop in range(start, end+1). We
will get each page on by one from the source pdf file and then add
it to the pdf_writer object. So in line 18, we get the page at the
current index, i , as: page = pdf.getPage(i). And in line 21, we
add this page to the pdf_writer as: pdf_writer.addPage(page) .
When the loop is finished we will have all the pages added to the
pdf_writer .
In line 25, we open a new PDF file in the specified path as:
with open(new_pdf_path, 'wb') as result. Since we use ‘wb’ as
the mode, it will create a blank file for us. We name this file as
result and we will fill with the pages from the pdf_writer . That’s
why, in line 26, we call the pdf_writer.write(result) method to
write the pages into the result file. And finally, as soon as the
with context manager ends, we will get our new file saved in the
project.
Now we can call this function in the main.py file. We will call
it with page numbers from 2 to 8 as:
pdf_operations.extract_pages(2,8) . And it will create a new file
in the pdf folder as: 2_8.pdf .

[24]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 pdf_operations.extract_pages(2,8)

Merge PDF Files:


In the next function we will see how to merge multiple PDF
files. The function name will be pdf_merge and its signature will
be as: pdf_merge(*args) . Since we don’t know which PDF files
to merge, we need to use the unknown parameters, *args , for this
function. We assume that, the items in this *args parameter will
be PDF files. We will be using the PdfFileMerger from PyPDF2
package. Let’s define our function:

[25]: 1 #--- merge PDF files ---#


2
3 def pdf_merge(*args):
4
5 # get the class
# TODO import PdfFileMerger from PyPDF2
6
package
7
8 # merger object
merger = #TODO instantiate PdfFileMerger
9
object
10
11 # *args
12 #TODO loop over args and append into merger
13
14 # save merger as pdf
merged_pdf_path = os .path.join(project_path,
15
'pdf', 'merged_pdf.pdf')
16
with #TODO open merged_pdf_path in write-
17 binary mode:
18 merger.write(merged_pdf)

We define the pdf_merge function in cell 25. We start by


importing the PdfFileMerger class from PyPDF2 .
In line 8, we instantiate an object from PdfFileMerger class
and name it as merger . This object will hold the PDF files that we
want to merge.
In line 11, we set a for loop on the args parameter. And we
iterate over each element in it, which we simple call arg . Since
each arg is a PDF file we will append this file to the merger as:
merger.append(arg) .
In line 15, we create a path for the final PDF file after merge
operation. We name this variable as merged_pdf_path and the
PDF file name will be merged_pdf.pdf .
In line 18, in the with context manager, we open the
merged_pdf_path file in ‘wb’ mode which will create a blank
PDF file for us. We name this file as merged_pdf .
Finally, in line 19, we write the content of merger object into
our merged_pdf file. And that’s it. We merged multiple PDF files
and save the resulting file as merged_pdf in the pdf folder.
Let’s call this function in the main.py file. We will pass two
PDF files to it, which are ‘pdf/8.pdf ’ and ‘pdf/2_8.pdf ’ . And it
will merge them and save the result. Please be careful that, these
two files must be present in the pdf folder before you call the
function.

[26]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 # pdf_operations.extract_pages(2,8)
pdf_operations.pdf_merge('pdf/8.pdf',
13
'pdf/2_8.pdf')

Rotate PDF:
In the next function we will see how we can rotate PDF files
either clockwise or counter clockwise. We will be using the
PdfFileMerger from PyPDF2 package. Let’s rotate.

[27]: 1 #--- rotate PDF files ---#


2
3 def pdf_rotate(degree):
4 """
5 Rotates the PDF file.
6 .rotateClockwise() and
7 .rotateCounterClockwise()
8 :param degree: int, must be multiples of 90
9 :return: None
10 """
11
#TODO import PdfFileReader, PdfFileWriter
12
from PyPDF2 package
13
14 # read the merged_pdf
merged_pdf = #TODO read 'pdf/merged_pdf.pdf'
15
with PdfFileReader
16
17 # create a writer
18 writer = #TODO instantiate PdfFileWriter object
19
20 # loop over the pages
for #TODO loop over number of pages in
21
merged_pdf:
22
23 # get the page
24 page = #TODO get page i in merged_pdf
25
26 # rotate the page
#TODO rotate page Clockwise in the given
27
degree
28
29 # pass this page to writer
30 #TODO add this page to writer
31
with #TODO open 'pdf/rotated.pdf' in write-
32
binary mode:
33 writer.write(rotated)

The function name is pdf_rotate and it takes the degree of


rotation as the parameter. We assume the degree to be a multiple of
90, because the PdfFileReader only works with these degrees.
In line 16, we create an instance of PdfFileReader class by
giving it the path which points to merged_pdf.pdf file. It is the
file we created in the previous function and we will use it for
rotation. We name the reader object as merged_pdf .
In line 19, we create a writer object by calling PdfFileWriter
class. Actually we don’t call classes, we call the constructors of
classes. Even if, you hear the term of “calling a class” it actually
means “calling the constructor of a class”. Never mind, you will
learn about classes in OOP (Object Oriented Programming)
chapter.
In line 22, we have a for loop on the range of the pages in the
merged_pdf.pdf file. We get the number of pages by calling
getNumPages() method. Here is the range:
range(merged_pdf.getNumPages()) .
In line 24, we get the current page at index i as: page =
merged_pdf.getPage(i) .
In line 27, we rotate this page clockwise as:
page.rotateClockwise(degree) .
In line 30, we add the rotated page to the writer object as:
writer.addPage(page) .
In line 33, in the with context manager, we open a new file in
‘wb’ mode with name rotated.pdf in the pdf folder. Here is the
code: with open('pdf/rotated.pdf', 'wb') as rotated. We name
the new file object as rotated .
And finally in line 34, we write the content of the writer object
to the rotated file. And this gives us a new PDF file whose pages
are rotated by the given degrees.
If you call this function in the main.py file, you will see a new
file named rotated.pdf in the pdf folder. Please make sure that,
merged_pdf.pdf file is be present in the pdf folder before you
call the function.

[28]: 1 # main.py file


2
3 import csv_operations
4 import pdf_operations
5
6 if __name__ == '__main__':
7 ...
8 # pdf_operations.read_pdf_properties()
9 # pdf_operations.pdf_read_page(2)
10 # pdf_operations.pdf_read_pages(62, 68)
11 # pdf_operations.extract_page(8)
12 # pdf_operations.extract_pages(2,8)
# pdf_operations.pdf_merge('pdf/8.pdf',
13
'pdf/2_8.pdf')
14 pdf_operations.pdf_rotate(90)

And this is the end of our assignment on CSV and PDF files.
For the solutions you can check Chapter 7 Project 1 - Working with
PDF & CSV Files.
9. OOP
Object-Oriented Programming (OOP) is a programming
paradigm based on the concept of "objects", which can contain
data and code: data in the form of fields (often known as attributes
or properties), and code, in the form of procedures (often known as
methods).
OOP uses objects and classes in programming, which aims to
implement real-world entities like inheritance, polymorphism,
encapsulation, etc. The main idea of OOP is to bind the data and
the functions that work together as a single unit. It is a widely used
programming paradigm to write powerful applications. It models
complex things as reproducible, simple structures that enables code
reusability, scalability and efficiency.
In this chapter we will learn all of the fundamental concepts of
OOP in Python. We will start with OOP basics, then we will learn
inheritance, encapsulation and polymorphism. We will move on
with multiple inheritance, method resolution order and finally
operator overloading.
We will create a new PyCharm project for this chapter. The
project name is OOP. You are recommended to create a new
project with a new virtual environment. You can also download the
project from the Github Repository of the book if you want. Here is
the project that we will develop in this chapter:
Figure 9-1: OOP project for this chapter

As you see in Figure 9-1 we have a main.py file in this


project. For each section we will create a separate Python file and
we will import them in the main.py file as modules to be able to
run the code in them. To start with we will create a Python file with
name _1_oop_basics.py . Make sure you import it in the
main.py file as:

# main.py file

if __name__ == '__main__':
import _1_oop_basics
OOP Basics
Python is an object oriented programming language. Almost
everything in Python is an object.
In Python objects have:
Attributes (properties)
behaviors (methods)

Let’s think with the help of a real world example. A penguin,


has attributes like: name, age, height, weight, color, family etc.
These are the properties that define a penguin. It also has behaviors
like: swim, walk, sing, dance, etc.

Class:
A class is a blueprint for creating objects. It is the template for
creating objects. It tells how the objects are going to look like.
For example: A house in real word is an Object. And the
architectural drawing of that house is the Class.
To create a class in Python, use the class keyword. Then we
give a name to the class and we open the class scope with a colon
( : ). Here is an example syntax: class MyFirstClass:.
Let’s define a class for the Penguin we talked about:

[1]: 1 # _1_oop_basics.py file


2
3 # class definition
4 class Penguin:
5 pass

In cell 1, you see a basic class definition. The class name is


Penguin and it has nothing in it. We just put the pass keyword
here to avoid getting any errors. Because we have to write at least
one executable line of code in the indented class block.

Object:
An Object is an instance of the Class. Instantiate is a technical
term and it means “creating Objects from Classes”. We create
Object by calling Classes. Actually by calling class constructors,
which we will see later in this chapter.
We have a Penguin class that we defined. Now, let’s create an
object out of this class:

[2]: 1 # create a Penguin object -> peng


2 peng = Penguin()

In cell 2, we create an object called peng . This object is created


from the class Penguin, by calling it as: Penguin() . In other words,
we instantiate an object peng from the class Penguin .

Class Attribute:
The attributes that all the Penguins have. Their scientific family,
for example. All the Penguins are from Spheniscidae family.
Another example for the class attribute for the Penguins might be
number of legs. All the Penguins have 2 legs regardless of their
species. Class attributes are defined at class level which means they
are not nested in any method.

Instance Attribute:
The attributes which are special to each individual Penguin:
age, color, height, weight, etc. Instance attributes are owned by the
specific instances (objects) of a class. Instance attributes are
defined in methods. So they are not class level, they are instance
level.
Method:
Methods are Functions which are defined in a Class. Methods
are the behaviors of the object. Methods can also be Instance
Methods and Class Methods.

Now that we know the basic terms about OOP, let’s do some
examples to fully understand them. We will start by defining our
Penguin class.

[3]: 1 class Penguin:


2 # class attribute
3 family = 'Spheniscidae'
4
5 # instance attributes
6 def __init__(self, name, age, color):
7 self.name = name
8 self.age = age
9 self.color = color

In cell 3, you see the complete definition of the Penguin class.


Here, family is the class attribute. You can think of it as a constant
that live with every object you create from this class. The scientific
family of the Penguins is Spheniscidae.
In line 6, you see a special method called __init__() . This is
the constructor for the classes in Python, which we will see in
detail in a minute. The parameters of the __init__() method are:
self , name , age , color . This means that, when you create a
Penguin you should pass its name , age , color . Here, one
parameter is different from the others and it’s extremely important.
It is the first parameter, self .
self :
It represents the current object that is being created. It is a
reference to the current instance of the class, and is used to access
variables that belongs to the class. It does not have to be named as
self , you can call it whatever you like, but it has to be the first
parameter of any function in the class.
In the __init__() method, in our Penguin class, we assign the
incoming parameters to the current object which we simply call as
self . The first assignment is: self.name = name. Here self.name
statement creates an instance attribute called name and assigs the
value of the name parameter. The same goes for self.age and
self.color too.
Now let’s create some instances of the Penguin class and see
how we access their attributes:

[4]: 1 # create two Penguin objects


2 king = Penguin( 'King Penguin', 4, 'Orange')
yellow_eyed = Penguin( 'Yellow-Eyed Penguin', 1,
3
'Brown')

In cell 4, we create two instances of the Penguin class. So we


have two objects. We name the first object as king and we pass the
parameters as: Penguin('King Penguin', 4, 'Orange'). This code
actually calls the __init__() method. Because __init__() method
is the class constructor. And for the parameters, name , age ,
color , we pass values as: 'King Penguin', 4 , 'Orange' . Please
be careful that, we do not pass anything for the first parameter,
which is self . We will talk on this later.
We name the second object as yellow_eyed and here is how
we create it: yellow_eyed = Penguin('Yellow-Eyed Penguin', 1,
'Brown') .
Now that we have two objects created, let’s print their
attributes. Let’s start by printing the class attributes:
[5]: 1 # print the class attributes
print('Scientific Family of {0} is
2
{1}'.format(king.name, king.__class__.family))
print('Scientific Family of {0} is
3 {1}'.format(yellow_eyed.name,
yellow_eyed.__class__.family))

[5]: Scientific Family of King Penguin is Spheniscidae


Scientific Family of Yellow-Eyed Penguin is
Spheniscidae

In cell 5, we call the attributes of our Penguin objects. Both


instance and class attributes.
Let’s start with instance attributes. Remember that, name , age
and color where the instance attributes in our Penguin class. Why?
Because we defined them in the __init__() method, not at the
class level. To access an instance attribute of an object, the syntax
is: object.attribute . For example to access the name of the king
object, we type as: king.name . And to access the name of the
yellow_eyed object we type as: yellow_eyed.name .
To access a class attribute we use the syntax of:
object.__class__.attribute . Here __class__ tells that, this is a
class attribute. In our Penguin class we have only one class
attribute which is called family . For the king Penguin we retrieve
it as: king.__class__.family . And for the yellow_eyed , we get it
as: yellow_eyed.__class__.family . In the output, you see that,
their values are the same which is Spheniscidae .
You can also access the class attributes without specifying the
__class__ attribute. See the code in the next cell:

[6]: 1 # class attributes without __class__


2 print('Scientific Family of {0} is
{1}'.format(king.name, king.family))
print('Scientific Family of {0} is
3
{1}'.format(yellow_eyed.name, yellow_eyed.family))

[6]: Scientific Family of King Penguin is Spheniscidae


Scientific Family of Yellow-Eyed Penguin is
Spheniscidae

As you see in cell 6, we access the class attributes without


typing __class__ as: king.family or yellow_eyed.family .
Although this possible, it is recommended to use __class__
attribute to distinguish class attributes from instance attributes.
In the next cell, let’s get all the instance attributes for both
objects:

[7]: 1 # print all instance attributes


print('Age of {0} is {1}, and its color is
2
{2}'.format(king.name, king.age, king.color))
print('Age of {0} is {1}, and its color is
3 {2}'.format(yellow_eyed.name, yellow_eyed.age,
yellow_eyed.color))

[7]: Age of King Penguin is 4, and its color is Orange


Age of Yellow-Eyed Penguin is 1, and its color is
Brown

In cell 7, we access the instance attributes of the objects as:


king.name , king.age , king.color . And in the same way for the
yellow_eyed Penguin.

__init__(self, ....):
When we create an object from a class, the first method which
is called is the __init__() method. It is known as the 'constructor'
method. In other words, __init__() creates an instance of the class.
In Python, every class has an __init__() method. Either you define
it explicitly or it is defined implicitly by Python, it exists in every
class. The first parameter in the __init__() method will always be
self (just like every other method in a class). After that you can
declare any arguments you want your class to accept.
Now that we know more about the building blocks of classes
and objects let’s redefine our Penguin class and give it more
functionality.

[8]: 1 # Add methods to our Penguin Class


2 class Penguin:
3 # class attributes
4 family = 'Spheniscidae'
5
6 # instance attributes
7 # constructor
8 def __init__(self, name, age, color):
9 self.name = name
10 self.age = age
11 self.color = color
12
13 # methods -> behaviors
14 # first parameter -> self
15 def swim(self):
16 return f '{self.name} can swim.'
17
18 def sing(self, can_sing=False):
19 if can_sing:
20 return f '{self.name} can sing.'
21 else:
22 return f '{self.name} can NOT sing.'
23
24 def dance(self, can_dance=False):
25 if can_dance:
26 return f '{self.name} can dance.'
27 else:
28 return f '{self.name} can NOT dance.'

In the Penguin class in cell 8, we have a class attribute as


family , then we have the __init__() method which takes four
parameters. The first parameter is always the self . We define
instance attributes inside the __init__() method as self.name ,
self.age , self.color . Their values are assigned from the
parameters.
In line 15, we have a method called swim . It has only one
parameter, which is the self . It simply return an f-string with
self.name in it as: return f'{self.name} can swim.'.
In line 18, there is another method called sing , which has two
parameters. The first parameter is the self again and the second
one is a boolean to decide if this Penguins can sing or not. If it can,
then it returns as f'{self.name} can sing.'. If not then returns as
f'{self.name} can NOT sing.'.
In line 24, you see the last method in our class, which is dance .
It takes two parameters as: self , can_dance . The default value for
can_dance is False . It checks if this Penguin can dance or not and
returns a text accordingly.
This Penguin class is a blueprint, which tells us how a Penguin
will look like. Now, let’s create two Penguin objects out of it, a
Rockhopper and a Happy Feet.

[9]: 1 #---- Object 1 -> Rockhopper ----#


2 # create new Penguin objects
3 rockhopper = Penguin( 'Rockhopper Penguin', 8,
'Yellow-Brown')
4
5 # let's see if rockhopper can swim
6 # rockhopper.swim() -> Penguin.swim(rockhopper)
7 print(rockhopper.swim())
8
9 # # is rockhopper can sing
10 print(rockhopper.sing(True))
11
12 # # is rockhopper can dance
13 print(rockhopper.dance(can_dance=False))

[9]: Rockhopper Penguin can swim.


Rockhopper Penguin can sing.
Rockhopper Penguin can NOT dance.

In cell 9, we create a Penguin object as rockhopper . We call


the Penguin class by passing a name, an age and a color as the
parameter values. We already know that, by the term “calling a
class” we actually mean “calling the constructor of the class
which is __init__() method.”. And these three are the parameters
of the __init__() method.
In line 7, we call the swim() method of the rockhopper
object. And in the output you see that, it returns as ‘Rockhopper
Penguin can swim.’.
In line 10, we call the sing method as:
rockhopper.sing(True) . And as you see we pass the value of
True to the can_sing parameter. And as you see in the output, it
returns as ‘Rockhopper Penguin can sing.’.
In line 13, we call the dance method as:
rockhopper.dance(can_dance=False) . We pass the value of
False for the can_dance parameter. So it returns as ‘Rockhopper
Penguin can NOT dance.’.
Now let’s define another Penguin object which means another
instance of the Penguin class:

[10]: 1 #---- Object 2 -> Happy Feet ----#


2 happy_feet = Penguin( 'Happy Feet', 1, 'Gray')
3
4 print(happy_feet.swim())
5
6 print(happy_feet.sing(can_sing=False))
7
8 print(happy_feet.dance(can_dance=True))

[10]: Happy Feet can swim.


Happy Feet can NOT sing.
Happy Feet can dance.

In cell 10, we create another Penguin instance with name


happy_feet , which can swim like all Penguins. Be careful that,
the swim() method has no parameters and it works in the same
way for all Penguins. happy_feet cannot sing:
happy_feet.sing(can_sing=False) . But it can dance:
happy_feet.dance(can_dance=True) .
Introduction to Inheritance
In object-oriented programming, inheritance is the mechanism
of deriving (inheriting) the properties from another class. It allows
us to define a child class that inherits all the methods and properties
from a parent class.

Parent Class: the class being inherited from, also called base
class or super class.
Child Class: the class that inherits from another class, also
called derived class or sub class.

Inheritance is one of the most powerful concepts in object


oriented programming, which help us to represent real-world
relationships in programming. It improves efficiency by providing
code reusability.
Let’s learn inheritance with an example. In this example, we
will define two classes: Bird and Owl .

[11]: 1 # Define Bird Class


2 class Bird:
3 def __init__(self):
4 print('Bird is created.')
5
6 def whoAmI(self):
7 print('I am a Bird.')
8
9 def fly(self):
10 print('Birds can fly.')
11
12 def swim(self):
13 print('Birds can swim.')
In cell 11, we have the definition of the Bird class. It has the
__init__() method, which just prints a text stating that 'Bird is
created.' . And it has another method named whoAmI , that prints
as 'I am a Bird.'. The third method is called fly , and it prints as
'Birds can fly.'. And the last method, swim , prints as 'Birds can
swim.' . As you see, these are basic and simple methods. Nothing
special so far. Let’s create a Bird object:

[12]: 1 # create a Bird instance


2 birdy = Bird()
3 birdy.whoAmI()
4 birdy.fly()
5 birdy.swim()

[12]: Bird is created.


I am a Bird.
Birds can fly.
Birds can swim.

In cell 12, we create a Bird object with name birdy . In line 2,


we call the __init__() method to create it as: birdy = Bird().
When this code is executed, the __init__() method runs and it
prints as ‘Bird is created.’.
In line 3, we call the whoAmI method on the birdy object as:
birdy.whoAmI() . And it prints as ‘I am a Bird.’.
In line 4, we call the fly method on the birdy object as:
birdy.fly() . And it prints as ‘Birds can fly.’.
In line 5, we call the swim method on the birdy object as:
birdy.swim() . And it prints as ‘Birds can swim.’.
Actually this Bird class represents the parent class for all of the
bird types. It is a generic class which tells the general properties of
birds. Owl is a bird too, so we can think of it as a child class of the
Bird . In other words, Owl inherits from Bird. Let’s define this
inheritance now:

[13]: 1 # child class


2 class Owl(Bird):
3
# We write parent class/classes inside
4
parenthesis
5
6 def __init__(self):
# first -> we call __init__() of Super class
7
(super() == Bird)
8 super().__init__()
9 print('Owl is created.')
10
11 # override the parent methods
12 def whoAmI(self):
13 print('I am an Owl.')
14
15 # Since all the birds can fly -> Owl also can fly
16 # leave fly method as it is -> do not override
17
18 def swim(self):
19 print('Owls can not swim.')
20
21 # Owls have night vision
22 def night_vision(self):
23 print('Owls have night vision.')

In cell 13, we define the Owl class which inherits from the
Bird class. In class definition, the parent class is written inside the
parentheses as: class Owl(Bird). Now the Bird is the parent class
and the Owl is the child class. Owl inherits all the properties and
methods from its parent. It can use them as they are or it can
override (redefine) them. It can also add new attributes or methods
which are not present in its parent.
In our example, we override __init__() , whoAmI() and
swim() methods. We do not redefine the fly() method, which
means we use it as it is. We have a new method in the Owl class
which doesn’t exist in its parent. This is the night_vision()
method. Night vision is special to the Owls, it is not common for
all the Birds. So we add this functionality to the Owl class only.
In the __init__() method of the Owl class we have a special
method call which is super() . Let’s first see its definition.
super() :
In Python, the super() method lets you to access methods in the
parent class. It returns a temporary object of the super class that
then allows you to call that super class’s methods.
In the __init__() method of the Owl class we call the
__init__() method of its parent as: super().__init__() . Here
super() simply means the Bird class.
This is a very common pattern in OOP for the __init__()
methods. You first call the __init__() method of the parent, then
you add custom functionality for your child class.
Now let’s create an Owl object and see inheritance in action:

[14]: 1 # create an Owl


2 owl = Owl()
3 owl.whoAmI()
4 owl.fly()
5 owl.swim()
6 owl.night_vision()

[14]: Bird is created.


Owl is created.
I am an Owl.
Birds can fly.
Owls can not swim.
Owls have night vision.

In cell 14, we instantiate an Owl object as: owl = Owl(). As


we know already, as soon as we call the class, the __init__()
method is executed. And inside the __init__() method of the Owl
class, we first call the __init__() method of its parent, which is the
Bird class. Remember the line of super().__init__() . This line
prints as ‘Bird is created.’, which is the first sentence you see in
the output. After that, __init__() method of the Owl prints as
‘Owl is created.’, which is the second sentence.
In line 3, we call the whoAmI method on the owl object as:
owl.whoAmI() . Since we override this method it prints as ‘I am
an Owl.’.
In line 4, we call the fly method on the owl object as:
owl.fly() . We did not override this method which means we use it
as it is in the parent class. That’s why it prints as ‘Birds can fly.’.
In line 5, we call the swim method on the owl object as:
owl.swim() .Since we override this method it prints as ‘Owls can
not swim.’.
And finally in line 6, we call our new method which is special
to the Owl class as: owl.night_vision() . This method prints as
‘Owls have night vision.’.
Let’s prove that night_vision() method does not exist in the
Bird class. Since we have an object of that class as birdy , let’s try
to call the night_vision() method on it:

[15]: 1 # birdy is a Bird but not an Owl


2 birdy.night_vision()
AttributeError: 'Bird' object has no attribute
[15]:
'night_vision'

As you see in the output of cell 15, we get an AttributeError


stating that: 'Bird' object has no attribute 'night_vision'.
Because night_vision() is a method which is defined in its child
class not in itself. So the parent doesn’t know anything about its
child’s implementation.
This proves us that: inheritance works downwards (from
parent to child), it does not work upwards (from child to parent).
Before finalizing this section let’s draw our two classes to see
the inheritance:

Figure 9-2: Parent – Child classes and inheritance

.
Encapsulation
In some situations, we may not want anyone to access the
attributes in our class. We may want to control the access. This is
done via Encapsulation.
In OOP terminology, encapsulation is the packing of attributes
and methods within a single object. By doing so, we can hide the
internal state of the object from the outside. Encapsulation provides
information hiding.
Private Attributes: Attributes that are only accessible inside
the class. No access from the outside. To make an attribute private
we use a prefix of double underscores as: __attribute . Let’s see
an example:

[16]: 1 # Let's assume we have a Telephone class


2 class Telephone:
3 def __init__(self):
# let's define the standard price of the
4
telephone -> private attribute
5 self.__price = 1000
6
7 def sell(self):
print('Selling price is:
8
${}'.format(self.__price))
9
10 # setter
11 def set_price(self, new_price):
12 # check if the price is negative
13 if new_price <= 0:
14 print('Price must be POSITIVE.')
15 else:
16 self.__price = new_price
17
18 # getter
19 def get_price(self):
20 return self.__price

In cell 16, we define a class named Telephone . In the


__init__() method we create a private attribute as __price and we
set its value as: self.__price = 1000. This is the initial price for
any Telephone object. Keep in mind that this __price attribute is
private, so no one outside the class can access it. So we have to
find a way to set the Telephone price.
In line 7, we have a method called sell . This method prints the
current price of the Telephone object as: 'Selling price is:
${}'.format(self.__price) .
In line 11, we the set_price() method which takes the new
price as the parameter. Here is the definition: def set_price(self,
new_price) . The purpose of this method is to set a new value to
the __price attribute. This new value is going to be the
new_price parameter. But we have a condition for this. We will
only set the price if the new_price is greater than zero. Otherwise,
we will print a text stating that 'Price must be POSITIVE.'. This
is the meaning of the if-else statement in the set_price() method.
By applying this logic, we gain the advantage to control the price
setting mechanism. We can prevent a negative number to be the
price value.
In line 19, we have another method called get_price . Since our
__price attribute is private, no one can see its value. So we need a
method to return the price if someone asks for it. This is the
get_price() method and it simply returns the price as: return
self.__price .
And that’s it. Now we implement a level of encapsulation in our
class. We have the complete control over our __price attribute.
Now let’s see this class in action:

[17]: 1 # create a Telephone object


2 phone = Telephone()

In cell 17, we instantiate a Telephone object named phone .


Now let’s access its price. How much is a phone?

[18]: 1 # we want to access its price


2 phone.__price

AttributeError: 'Telephone' object has no attribute


[18]:
'__price'. Did you mean: 'get_price'?

In cell 18, we try to access the price of our phone object as:
phone.__price . But we get an AttributeError sayin:
'Telephone' object has no attribute '__price'. Did you mean:
'get_price'? . Why do we get this error? Because __price is
private. You cannot access private attributes from outside of the
class. The correct way is to call the get_price() method to learn
the price, which we will do it in a minute.
Let’s call the sell() method:

[19]: 1 # sell the phone


2 phone.sell() # $1000

[19]: Selling price is: $1000

As you see in cell 19, we get the selling price of $1000 which
is the current value of the __price attribute.
Now you will see something interesting. Let’s set the price to
$5000. To do this, we just assign this value to the phone.__price .
Let’s do it and see what happens:

[20]: 1 # set the price -> $5000


2 phone.__price = 5000

In cell 20, we assign a new value to the __price attribute as:


phone.__price = 5000. Or at least, we think we assigned. You will
not get any errors when you run the code in cell 20. But there is
something wrong here. To see what it is, let’s call the sell()
method one more time:

[21]: 1 # sell the phone


2 phone.sell() # $1000

[21]: Selling price is: $1000

In cell 21, we call the sell() method after we think we assigned


the value of $5000 to the price. And yet, in the output you see that
the price is still $1000 . Which means, it hasn’t changed. I can hear
that, you ask as ‘How is that possible?’. It is possible, because we
didn't set the __price attribute in the class. When we type as
phone.__price , Python creates a new attribute with the same name
as __price for this phone object only. It has no effect on the class
level. And to be sure, let’s print the value of the phone.__price .

[22]: 1 # print the value of phone.__price


2 print(phone.__price)
# again we see as $1000 -> because we didn't set the
3
__price in the class
4 # phone.__price -> Python creates a new attribute

[22]: 5000
As you see in the output of cell 22, the value of phone.__price
is 5000 . But it has no effect on the __price attribute inside the
Telephone class. With this example, I wanted to show you, how it
might be tricky when you deal with private elements of a class.
You should be aware of what you are doing, and you should always
use get-set logic to manipulate private elements.
In Python, you can set attributes to the objects, independent of
their classes. For example, we can add a new attribute to our phone
object as color. Let’s do it:

[23]: 1 # You can set attributes to the objects,


2 # independent of their classes.
3 phone.color = 'Black'
4 print(phone.color)

[23]: Black

As you see in cell 23, we add a new attribute to our phone


object as: phone.color = 'Black'. But this has nothing to do with
the Telephone class. It is just an attribute of the current phone
object. If you create another instance of the Telephone class,
phone_2 let’s say, this one will not have this color attribute.
OK, we saw that we cannot set the value of the __price
attribute by just typing phone.__price = 5000. This didn’t work.
But how can we do this? How can we change the current price?
This is where the idea of get-set methods comes in.
Get-Set Methods: We use get-set (or getter-setter) methods, to
set and get the private attributes of a class.
Now let’s set the price to $8000 . To do this, we will call the
set_price() method and pass the new price value:
[24]: 1 # set the price to 8000
2 phone.set_price(8000)

In cell 24, we set a new value to the __price attribute by


calling the set_price() method. Let’s see if it really worked or
not? To see the current price, we need to call the get_price()
method.

[25]: 1 # get the current price


2 price = phone .get_price()
3 print('Current Price is:', price)

[25]: Current Price is: 8000

As you see in the output of cell 25, the price is updated as


8000 . Let’s also call the sell() method to make sure that the price
is 8000 :

[26]: 1 # sell the phone again


2 phone.sell() # $8000

[26]: Selling price is: $8000

As you see the selling price, which means the current value of
the __price attribute is 8000 . So our get-set logic worked
perfectly.
Now that, you learned the basic logic behind encapsulation, you
should answer the question of: Why do we need Encapsulation?
We need it, to give all the control to the class. The class can do
necessary checks in the set methods and can implement or reject
any modification it wants, regardless of the outside world.
As the last example, let’s say someone wants to set a negative
value for the price of a Telephone :

[27]: 1 # Ex:
2 # If you want to set a negative price
3 # Price -> -2000
4 phone.set_price(-2000)
5 price = phone .get_price()
6 print('Current Price is:', price)

[27]: Price must be POSITIVE.


Current Price is: 8000

As you see in cell 27, we try to set -2000 as the price and the
set_price() method returns as ‘Price must be POSITIVE.’. And
we see the price hasn’t changed we call the get_price() method. It
is still 8000 .
Polymorphism
Polymorphism is another important concept in OOP. The word
poly-morphism means having many forms. It refers to the same
object (or function) exhibiting different forms and behaviors in
different scenarios. In simple words, it means the same function
name behaving differently for different classes.
To be able to understand polymorphism, we will use three
classes: Bird , Owl and Penguin . Bird is the parent class, Owl
and Penguin are child classes. We have already defined the Bird
class in cell 11, so we will use it as it is. We will redefine the two
child classes, Owl and Penguin . Both will inherit from the Bird
class:

[28]: 1 # child class


2 class Owl(Bird):
# We write parent class/classes inside
3
parenthesis
4
5 def __init__(self):
# first -> we call __init__() of Super class
6
(super() == Bird)
7 # super().__init__()
8 print('Owl is created.')
9
10 # override the parent methods
11 def whoAmI(self):
12 print('I am an Owl.')
13
14 def fly(self):
15 print('Some owls can not fly.')
16
17 def swim(self):
18 print('Owls can not swim.')
19
20 # Owls have night vision
21 def night_vision(self):
22 print('Owls have night vision.')

In cell 28, you see the redefinition of the Owl class. In the
__init__() method it doesn’t call the super class. A class is free to
choose if it wants to call __init__() method of the super class or
not as: super().__init__() . We don’t need it in the Owl class
anymore, so we didn’t call the super() here. The __init__()
method in Owl class only prints as 'Owl is created.'.

[29]: 1 # penguin class


2 class Penguin(Bird):
# We write parent class/classes inside
3
parenthesis
4
5 def __init__(self):
6 # super().__init__()
7 print('Penguin is created.')
8
9 # override the parent methods
10 def whoAmI(self):
11 print('I am a Penguin.')
12
13 # override fly
14 def fly(self):
15 print('Penguins can not fly.')
16
17 # leave the swim method as it is
In cell 29, you see the Penguin class which is redefined. Now it
inherits from the Bird class as: class Penguin(Bird).
In the __init__() method it doesn’t call the super class. The
__init__() method in Penguin class only prints as 'Penguin is
created.' .
The Penguin class overrides the whoAmI() and fly() methods.
But it doesn’t touch the swim() method. It uses this one as it is in
its parent.
Now, we have two child classes inheriting from the same parent
class. Owl and Penguin are derived from the Bird class.
We will define a new function to see the polymorphism in
action. The function name will be can_it_fly and it will be a
common function that work on any object which is of type Bird or
its child classes.

[30]: 1 # common function


2 def can_it_fly(bird):
3 # call the fly method on the parameter bird
4 bird.fly()

As you see in cell 30, the function definition is very simple. It


takes one parameter as bird and calls the fly() method of this
parameter. Here is the code: bird.fly() . Here we assume, the bird
object has a method called fly .
We want to call this can_it_fly() function with three different
objects. The objects will be of type: Bird , Owl and Penguin .
First let’s create them:

[31]: 1 # create three objects -> Bird, Owl, Penguin


2 bird = Bird()
3 owl = Owl()
4 penguin = Penguin()

[31]: Bird is created.


Owl is created.
Penguin is created.

In cell 31, we define three objects. A bird , an owl and a


penguin . In the output, you see the result of the calls to the
__init__() methods respectively.
Now let’s call the can_it_fly() function with these objects.
Let’s name it as Fly Test:

[32]: 1 # call with three objects -> Bird, Owl, Penguin


2 print("#--- Fly Test ---#")
3 can_it_fly(bird)
4 can_it_fly(owl)
5 can_it_fly(penguin)

[32]: #--- Fly Test ---#


Birds can fly.
Some owls can not fly.
Penguins can not fly.

As you see in cell 32, we call the can_it_fly() function with


three objects and get different results. Each one calls its own
version of the fly() method. For example the bird says ‘Birds
can fly.’ but the owl says ‘Some owls can not fly.’ and the
penguin says ‘Penguins can not fly.’.
We have one single interface, which is our can_it_fly()
function here, but the result depends on the object we pass to it. In
other words, the same function( can_it_fly ) returns different
results based on the object as its parameter. This is what we call as
polymorphism.
Classes and Objects
In this section we will cover more concepts on classes and
objects and you will learn fundamentals of OOP in more detail.
For this section, we will create a new Python file with name
_2_classes_and_objects.py . In the main.py file, make sure you
comment out the previous file and import this one as:

[33]: 1 # main.py file


2
3 if __name__ == '__main__':
4 # import _1_oop_basics
5 import _2_classes_and_objects

In Python, classes can have docstrings like functions. A


docstring is a special purpose string which is used for
documentation. Let’s see a class definition with its docstring:

[34]: 1 # _2_classes_and_objects.py file


2
3 # Defining a class with docstring
4 class Car:
5 """
6 This is the car class.
7 """
8 # class attribute
9 brand = 'BMW'
10
11 def work(self):
12 print('This car works.')

In cell 12, we have the definition for the Car class. It starts
with the docstring. And in the docstring we state what this class is.
You can add more information if you want in the docstrings.
The Car class, has no explicit __init__() method. Which
means Python will create an implicit one for us.
There is a class attribute named brand and value of ‘BMW’ .
This means that every Car object we create will have their brand
as ‘BMW’.
There is a method in the class named as work . The method has
no parameters, excluding the self because it is the default one.
Let’s get the brand attribute on the class itself. Be careful that,
we will not create any objects. We will use the class name to get
the attribute:

[35]: 1 # to call the brand attribute -> class


2 print(Car.brand)

[35]: BMW

In cell 35, we access the brand attribute directly on the class


itself as: Car.brand . It is completely logical because the brand
attribute is a class attribute. This wouldn’t be possible if it was an
instance attribute. You would need an instance (object) to be able
to access an instance attribute.
dunder (double underscore): methods or attributes with two
underscore prefix as __attribute . In Python, there are built-in
dunder methods and attributes in each class.
For example, there is a dunder attribute for the docstring which
is : __doc__. Let’s see it for our Car class:

[36]: 1 # docstring of class


2 print(Car.__doc__)
[36]: This is the car class.

And there is a dunder attribute for the class name as:


__name__ . It gives the name of the class. Here it is for the Car
class:

[37]: 1 # class name


2 print(Car.__name__)

[37]: Car

Now let’s get some basic information of a method in a class.


For example, the info of the work() method in the Car class:

[38]: 1 # get details of work method


2 print(Car.work)

[38]: <function Car.work at 0x000001DD49F992D0>

As you see in the output, the type of Car.work is a function.


Methods are nothing but functions which are defined in a class.
Basically, they are the same.
Now let’s call the Car.work method. But we will call it
directly on the class itself. Not on an object. Let’s see what
happens:

[39]: 1 # call the work method on the class


2 Car.work()

TypeError: Car.work() missing 1 required positional


[39]:
argument: 'self'
In cell 39 we call the work() method on the class level as:
Car.work() . And it raises an exception saying that: TypeError:
Car.work() missing 1 required positional argument: 'self'.
Why? Because, the work() method has a parameter called self ,
which is mandatory. By mandatory we mean, there is no default
value for it, so you have to pass a value to it. Since we don’t
provide any argument for the self parameter while calling it, we
get an error. Actually these methods, which require a self
parameter is called instance methods.
If we want to be able to successfully call an instance method on
the class level, you have to pass an object to it for the self
parameter. We will see it in a minute. But first let’s call it on an
object:

[40]: 1 # define an object and call work() on it


2 new_car = Car()
3 print(new_car.brand)
4 new_car.work()

[40]: BMW
This car works.

In cell 40, we create an object by instantiating the Car class.


The object name is new_car and in line 3, we print the brand
attribute of it as: new_car.brand . In line 4, we call the work()
method on that object as: new_car.work() . And it prints as ‘This
car works.’. Everything works perfectly. But there is a question
here:
Question:
Where is the self parameter? We didn’t pass any self
parameter when we call the work() method as new_car.work() .
The parentheses of the work() method are empty. So, where is it?
Answer:
The self parameter is the new_car itself. When we say
new_car.work() , Python takes the new_car as the first parameter
to the work() method. So it is the self parameter. This also
explains why the self parameter always need to be the first
parameter for the instance methods in a class.
Now that we know, the self parameter is the object itself, we
can use it while calling the method on the class level. Remember,
in cell 39, we got an error when we call the work() method on the
class level as: Car.work() . Now we can fix it. All we have to do is
to pass an object of type Car in to the work() method. Let’s do it:

[41]: 1 # First parameter is 'self' -> new_car


2 Car.work(self=new_car)
3 Car.work(new_car)

[41]: This car works.


This car works.

In cell 41, we pass the new_car object as the argument to the


Car.work() function call. We can pass it either as a named
argument or without specifying a name. The first parameter is
always the self .
Now let’s do another example. We will define a new class
named Circle :

[42]: 1 # Ex
2 import math
3
4 class Circle:
5
6 def __init__(self, radius):
7 self.__radius = radius
8
9 def get_radius(self):
10 return self.__radius
11
12 def set_radius(self, new_radius):
13 if new_radius > 0:
14 self.__radius = new_radius
15 else:
16 print('Radius must be positive.')
17
18 def perimeter(self):
19 return math .pi * self.__radius**2

In cell 42, we define the Circle class. In the __init__()


method, it defines a private attribute as __radius . The value is
from the parameter called radius. Here is the code for it:
self.__radius = radius. Since the __radius is a private attribute,
we need to define getter-setter methods for it.
In line 9, we have the getter method as get_radius and it
simply returns the value of the self.__radius .
In line 12, we have setter method as set_radius . It takes the
new_radius as the parameter. And it checks if this new_radius is
greater than zero or not. If it is, then the value of the radius is
updated as: self.__radius = new_radius. If it’s not, then the
method prints as 'Radius must be positive.'.
In line 18, we have the last method named perimeter . It is a
one-line function that returns the perimeter of the circle as: return
math.pi * self.__radius**2.
Let’s create an object of type Circle and see the methods in
action:
[43]: 1 # create an object
2 circle_1 = Circle( 10)
3 print(circle_1.get_radius())
4 perimeter = circle_1 .perimeter()
5 print(perimeter)

[43]: 10
314.1592653589793

In cell 43, we create an object named circle_1 , by pass the


radius of 10 to the __init__() method as: circle_1 = Circle(10).
In line 3, we call the get_radius() method as:
circle_1.get_radius() . And the result is 10 , as expected.
In line 4, we get the perimeter of the circle by calling the
perimeter() method as: circle_1.perimeter() . Then we print the
result.
Now let’s set a new value for the radius of our circle_1 object:

[44]: 1 # print('----- set ------')


2 circle_1.set_radius(20)
3 perimeter = circle_1 .perimeter()
4 print(perimeter)

[44]: 1256.6370614359173

In cell 44, we set a new value for the radius as:


circle_1.set_radius(20) . And then we call the perimeter()
method and print the result, which is calculated with the new
radius.
Let’s define a new class with name Student . It’s going to be a
simple class with only the __init__() method in it.
[45]: 1 # define a new class
2 class Student:
3 def __init__(self, name, age, grade):
4 self.name = name
5 self.age = age
6 self.grade = grade

In cell 45, we have the definition of the Student class. The


__init__() method takes three parameters as name , age , grade .
We do not consider the self parameter, because it is the default
one. Each parameter is assigned to an instance attribute. Instance
attributes are: self.name , self.age and self.grade .
Now let’s create an instance of this Student class and print the
attributes:

[46]: 1 # instantiate std object


2 std = Student( 'John Doe', 8, '7th Grade')
3 print(std.name)
4 print(std.age)
5 print(std.grade)

[46]: John Doe


8
7th Grade

In cell 46, we create an object with name std out of the


Student class. And then we print its attributes.
Now let’s delete some attributes. In Python, you can add new
attributes to the objects and you can delete them. To delete an
attribute we use the syntax of: del object.attribute.

[47]: 1 # Delete attributes of objects -> del


2 del std .age

In cell 47, we delete the age attribute of the std object as: del
std.age . We will get an error if we want to print this attribute now.
Let’s see:

[48]: 1 # try to access after delete


2 print(std.age)

[48]: AttributeError: 'Student' object has no attribute 'age'

To delete the entire object we use the same syntax: del object.
Let’s delete the std object we created in cell 46.

[49]: 1 # delete std object


2 del std

And that’s it. The std object will be deleted after we run the
code: del std. And again, you will get an error if you try to access
it after deletion.

[50]: 1 # try to access after delete


2 print(std.age)

NameError: name 'std' is not defined. Did you mean:


[50]:
'str'?
Inheritance
In the previous sections, we learned the basics of inheritance in
OOP context. We will elaborate it more in this section.
For this section, we will create a new Python file with name
_3_inheritance.py . In the main.py file, make sure you comment
out the previous files and import this one as:

[51]: 1 # main.py file


2
3 if __name__ == '__main__':
4 # import _1_oop_basics
5 # import _2_classes_and_objects
6 import _3_inheritance

To start with, we will define three classes: SuperClass ,


ChildClass1 and ChildClass2 . As their names imply, the first one
is the parent class and the others are the child classes. They will not
have any implementation in them, only the pass keyword.

[52]: 1 # _3_inheritance.py file


2
3 class SuperClass:
4 # super class properties
5 pass
6
7 class ChildClass1(SuperClass):
8 # super class properties
9 # child class 1 properties
10 pass
11
12 class ChildClass2(SuperClass):
13 # super class properties
14 # child class 2 properties
15 pass

As you see in cell 52, the SuperClass has some properties


which we call as ‘super class properties’.
The ChildClass1 class inherits from the SuperClass , so it has
‘super class properties’ and ‘child class 1 properties’.
And the ChildClass2 class inherits from the SuperClass , so it
has ‘super class properties’ and ‘child class 2 properties’.

The object class:


In Python, there is a class which is the root of all classes. This is
the object class and it is the super class of all other classes. In
other words, all of the classes, either built-in or custom defined,
inherit from the object class.
Let’s do a real example now. We will define some classes for
the shapes. The parent class of all shapes will be the Shape class.
Then we will have Circle , Rectangle and Square as derived
classes.

[53]: 1 # Ex
2 import math
3
4 # super class -> Shape
5 class Shape(object):
6 """Super class for shapes."""
7 def __init__(self, color='red'):
8 self.color = color

In cell 53, you see the definition of the Shape class. It inherits
from the object class as: class Shape(object). Actually this
explicit inheritance is not necessary, because we already know that
every class inherits from the object class. So you do not need to
write it in the class parentheses. I did it intentionally, to show you
that, object is the parent class of all classes in Python.
There is an __init__() method in the Shape class, which
defines an instance attribute named color as: self.color = color.
The default value for the color parameter is ‘red’ .
Now let’s define the child (sub) classes:

[54]: 1 # sub class -> Circle


2 class Circle(Shape):
3 """Circle inherits from Shape."""
4 def __init__(self, radius, color='blue'):
5 super().__init__(color=color)
6 self.radius = radius
7
8 def area(self):
9 return math .pi * self.radius**2

We define the Circle class in cell 54. It is a sub class of the


Shape class. The __init__() method has two parameters: radius
and color . The default value for the color is ‘blue’ . It first calls
the __init__() method of its parent and passes the color as:
super().__init__(color=color) . Then it sets an instance attribute
as: self.radius = radius.
There is one more method in the Circle class, which is called
area . The area() method returns the area of the circle as: return
math.pi * self.radius**2.

[55]: 1 # sub class -> Rectangle


2 class Rectangle(Shape):
3 """Rectangle inherits from Shape."""
4 def __init__(self, width=1.0, length=1.0,
color='orange'):
5 super().__init__(color)
6 self.width = width
7 self.length = length
8
9 def area(self):
10 return self.width * self.length

In cell 55, we have the Rectangle class, which also inherits


from the Shape class. In its __init__() method it first calls the
__init__() method of its super class then it sets two attributes as
self.width and self.length . The default values for the parameters
are: width=1.0 , length=1.0 , color='orange' .
Then we have the area() method which returns the area of the
rectangle as: return self.width * self.length.

[56]: 1 # sub class -> Square


2 class Square(Shape):
3 """Square inherits from Shape."""
4 def __init__(self, side=1.0, color='white'):
5 super().__init__(color)
6 self.side = side
7
8 def area(self):
9 return self.side**2

In cell 56, we have the last shape, Square , which also inherits
from the Shape class. In its __init__() method it first calls the
__init__() method of its super class then it sets an attribute as
self.side . The default values for the parameters are: side=1.0 ,
color='white' .
Now that we have our classes defined, let’s create objects from
them:

[57]: 1 # Create Objects


2 # Shape
3 sh = Shape( 'purple')
4 print('Color of Shape sh:', sh.color)

[57]: Color of Shape sh: purple

In cell 57, we create an object called sh from the Shape class.


And we pass the color of 'purple' as the parameter. Here is the
code: sh = Shape('purple'). Then we print the sh.color attribute.

[58]: 1 # Circle
2 ci = Circle(radius =5)
3 print('Radius of Circle ci:', ci.radius)

[58]: Radius of Circle ci: 5

In cell 58, we instantiate a Circle object as ci . And we pass


the value of 5 for the radius parameter. Then we print the value
of ci.radius attribute. Now let’s print the color of the ci object:

[59]: 1 # color of Circle


2 # super().__init__(color=color) -> blue
3 print('Color of Circle ci:', ci.color)

[59]: Color of Circle ci: blue

In cell 59, we print the color attribute of the ci object. Be


careful that, we don’t define an attribute as self.color in the
__init__() method of the Circle class. And yet, we have that
attribute. Because it comes from the super class. When we call
super().__init__() in the Circle class, it creates an attribute called
self.color in the parent class. And since the Circle inherits from
that class, it has this attribute too. And the value for this attribute is
‘blue’ . Because in the Circle class, the function call is:
super().__init__(color=color) , and the default value for the
color parameter is ‘blue’ .

[60]: 1 # Rectangle
2 re = Rectangle(width =2, length=8, color='yellow')
3 print('Color of Rectangle re:', re.color)
4 print(re.area())

[60]: Color of Rectangle re: yellow


16

In cell 60, we create an object of type Rectangle . And we pass


the named arguments as: width=2 , length=8 , color='yellow' .
Then we print the color attribute of the re object as: re.color .
And finally we get the area as: re.area() .

Proof:
Let’s prove that the object class is the super class for all the
classes in Python. We created an object named sh in cell 57. It was
an instance of the Shape class. Now let’s use this object to check
if it is an instance of the object class:

[61]: 1 # is sh an instance of the object class


print('Is Shape a subclass of object class?: ',
2
isinstance(sh, object))
[61]: Is Shape a subclass of object class?: True

In cell 61, we call the isinstance() method as: isinstance(sh,


object) . As you see in the output, it returns True , which means sh
comes from the object class. In other words, the object class is
the root class for Shape class.

issubclass():
There is a built-in function in Python, which checks if a class is
a sub class of another class. Let’s check if the Square class is a
sub class of the Shape class:

[62]: 1 # issubclass()
2 # is Square class is subclass of Shape class
print('Is Square class is subclass of Shape class?: ',
3
issubclass(Square, Shape))

[62]: Is Square class is subclass of Shape class?: True


Multiple Inheritance
In Python, a class can inherit from multiple classes which we
call as Multiple Inheritance. It is very commonly used in software
development. In this section you will learn how multiple
inheritance work with examples.
We will create a new Python file with name
_4_multiple_inheritance.py . In the main.py file, make sure you
comment out the previous files and import this one as:

[63]: 1 # main.py file


2
3 if __name__ == '__main__':
4 # import _1_oop_basics
5 # import _2_classes_and_objects
6 # import _3_inheritance
7 import _4_multiple_inheritance

We will start with a very simple example, with three classes


which are; Main_1 , Main_1 and SubClass . As their names
imply, the first two will be the parent classes and the last one will
be the child class.

[64]: 1 # _4_multiple_inheritance.py file


2
3 # parent class 1
4 class Main_1:
5 pass
6
7 # parent class 2
8 class Main_2:
9 pass
10
11 # child inherits from two parents
12 class SubClass(Main_1, Main_2):
13 pass

As you see in cell 64, line 12, the SubClass inherits from two
parents as: SubClass(Main_1, Main_2). This is how we define
multiple inheritance. We just put the parent class names into the
parentheses. The names are separated by commas.
Now let’s do a real world example. First, we will define a
Clock class to keep track of the time. Then we will define a
Calendar class to simulate a physical calendar. And finally we will
define a CalendarClock which is the combination of both classes.

[65]: 1 # Ex
# Clock and Calendar -> CalendarClock will
2
inherit from both
3
4 # Clock Class
5 class Clock:
6 """Simulates the clock."""
7
8 def __init__(self, hours, minutes, seconds):
9 self.__hours = hours
10 self.__minutes = minutes
11 self.__seconds = seconds
12
13 def set_clock(self, hours, minutes, seconds):
14 self.__hours = hours
15 self.__minutes = minutes
16 self.__seconds = seconds
17
18 def time(self):
return '{0}:{1}:{2}'.format(self.__hours,
19
self.__minutes, self.__seconds)

In cell 65, you see the definition of the Clock class. It has three
attributes defined in the __init__() method which are:
self.__hours , self.__minutes , self.__seconds . All three of them
are private attributes, because they start with double underscores
( __ ). So, no one can access or modify them from outside the class.
That’s why we have a set_clock() method to be able to set new
values to them. And a time() method to display them in time
format.
Let’s create an object of type Clock , then set the time and
finally display it:

[66]: 1 # create a Clock object


2 clock = Clock( 0, 0, 0)
3 print(clock.time())
4 clock.set_clock(10, 4, 28)
5 print(clock.time())

[66]: 0:0:0
10:4:28

In cell 66, we instantiate a Clock object with 0 as the value for


all the parameters which are hours , minutes and seconds . Then
in line 4, we call the set_clock() method and pass some values for
the attributes as: clock.set_clock(10, 4, 28). And in line 5, we
call the clock.time() method to get the current time.
Let’s define the second class which is the Calendar .

[67]: 1 # Calendar Class


2 class Calendar(object):
3 """Simulates the calendar"""
4
5 def __init__(self, d, m, y):
6 self.set_calendar(d, m, y)
7
8 def set_calendar(self, d, m, y):
9 self.__d = d
10 self.__m = m
11 self.__y = y
12
13 def date(self):
return '{d}:{m}:{y}'.format(d=self.__d,
14
m=self.__m, y=self.__y)

In the definition of the Calendar class in cell 67, you see a


different pattern for creating instance attributes. In the __init__()
method we simply call the set_calendar() method and pass the
parameters as: self.set_calendar(d, m, y). Be careful that we call
it as self. set_calendar(), since it expects the first parameter to be
the self . This is the way we call the instance methods in the class.
Keep this in mind.
In the set_calendar() method we set three private attributes as:
self.__d , self.__m , self.__y . And in the date() method we return
the formatted date. Now let’s create a Calendar object:

[68]: 1 # create a Calendar object


2 calendar = Calendar( 6, 11, 2019)
3 print(calendar.date())
4 calendar.set_calendar(16, 1, 2020)
5 print(calendar.date())

[68]: 6:11:2019
16:1:2020

In cell 68, we instantiate a Calendar object as calendar and


pass some values for day , month and year parameters. Then we
print the formatted date by calling the calendar.date() method in
line 3. In line 4, we set a new date as: calendar.set_calendar(16,
1, 2020). And finally we print that date too.
Now that we have to parent classes as Clock and Calendar ,
let’s create a new class which inherits from these two. Its name will
be CalendarClock and we will combine the time and date in this
new class.

[69]: 1 # CalendarClock Class


2 class CalendarClock(Clock, Calendar):
3 """Keeps calendar and clock together."""
4
def __init__(self, day, month, year, hours, minutes,
5
seconds):
6 # call the super classes init methods
7 Clock.__init__(self, hours, minutes, seconds)
8 Calendar.__init__(self, day, month, year)

In cell 69, we have the definition of the CalendarClock class


which has multiple inheritance. It inherits from two classes as:
CalendarClock(Clock, Calendar).
In its constructor, __init__() method, it takes all the necessary
data as parameters, which are: day , month , year , hours ,
minutes , seconds . These are needed for the Clock and
Calendar classes. Now it should call the __init__() methods of its
parents. But be careful here! We cannot use super().__init__() to
call the parent because there are multiple parents (supers). So we
need to type their names to call their __init__() methods.
Here is how we call them:
To call Clock class: Clock.__init__(self, hours, minutes,
seconds) .
To call Calendar class: Calendar.__init__(self, day, month,
year) .
That’s all. Since both parents have necessary implementation in
their constructors we now have time and date in our
CalendarClock class. And all the date and time methods too. Let’s
use them:

[70]: 1 # create a CalendarClock object


calendar_clock = CalendarClock( 8, 12, 2020, 16, 45,
2
7)
3 print(calendar_clock)
4 print(calendar_clock.time())
5 print(calendar_clock.date())

<_4_multiple_inheritance.CalendarClock object at
[70]:
0x0000019E31C2C040>
16:45:7
8:12:2020

And you see our CalendarClock object created in cell 70.


Since it inherits from both of the Calendar and Clock classes, it
gets everything they have. So we can call the
calendar_clock.time() method on it and it returns the time. We
can also call the calendar_clock.date() method to get the current
date. Multiple Inheritance work perfectly as you see.
Method Resolution Order (MRO)
In the previous section we learned about Multiple Inheritance.
There is one more topic that we will cover on inheritance which is
Multi-Level Inheritance.
Multiple Inheritance is not the same as Multi-Level Inheritance.
In Multiple Inheritance we have one child class that inherits
from multiple parents.
In Multi-Level Inheritance the parents also have parents,
and their parents have parents. So there is a deep inheritance
structure which we call multi-level.
A structure like: GrandParent > --- > GrandParent >
Parent > Child
We will learn about Multi-Level Inheritance and Method
Resolution Order (MRO) in this section.
We will create a new Python file with name _5_mro.py . In the
main.py file, make sure you comment out the previous files and
import this one as:

[71]: 1 # main.py file


2
3 if __name__ == '__main__':
4 # import _1_oop_basics
5 # import _2_classes_and_objects
6 # import _3_inheritance
7 # import _4_multiple_inheritance
8 import _5_mro

Let’s define some classes to understand the concept of Multi-


Level Inheritance. Remember that, we talked about the object
class which is the root (parent) class of all classes in Python. Since
almost everything in Python is an object, you can think the object
class as the mother of all classes.

[72]: 1 # _5_mro.py file


2
3 class GrandParent(object):
4 # GrandParent properties
5 pass
6
7 class Parent(GrandParent):
8 # GrandParent properties
9 # Parent properties
10 pass
11
12 class Child(Parent):
13 # GrandParent properties
14 # Parent properties
15 # Child properties
16 pass

In cell 72, you see a basic structure for multi-level inheritance.


The GrandParent class inherits from the object class, the
Parent class inherits from the GrandParent and the Child class
inherits from the Parent . As you see, the Child class has all the
properties of all of its ancestors.
Let’s do a real world example now. We will revisit our Clock
and Calendar classes to give them multi-level inheritance. We
will have the SandWatch class as the parent of the Clock .
Here is the inheritance structure for our classes:
object > SandWatch > Clock
object > Calendar
Clock & Calendar > CalendarClock
[73]: 1 # Example
2 # object > SandWatch > Clock
3 # object > Calendar
4 # Clock & Calendar > CalendarClock
5
6 # SandWatch Class
7 class SandWatch(object):
8 def __init__(self):
9 self.start_sand_watch()
10
11 def start_sand_watch(self):
12 print('Sand Watch started.')

In cell 73, we see the definition of the SandWatch class. It


inherits from the object class. And in its __init__() method it
calls an instance method as: self.start_sand_watch() . In the
start_sand_watch() method it just prints as 'Sand Watch
started.' .

[74]: 1 # Clock Class


2 class Clock(SandWatch):
3 """Simulates the clock."""
4
5 def __init__(self, hours, minutes, seconds):
6 super().__init__()
7 self.set_clock(hours, minutes, seconds)
8
9 def set_clock(self, hours, minutes, seconds):
10 self.__hours = hours
11 self.__minutes = minutes
12 self.__seconds = seconds
13
14 def time(self):
return '{0}:{1}:{2}'.format(self.__hours,
15
self.__minutes, self.__seconds)
16
17 # override start_sand_watch
18 def start_sand_watch(self):
19 print('Sand Watch started in Clock Class.')

We redefine our Clock class in cell 74. Now it inherits from


the SandWatch class. In the __init__() , method we call
super().__init__() to fire the __init__() method of its parent.
The set_clock() and time() methods are the same as the
previous definition. What’s new here is the start_sand_watch()
method. The Clock takes this method from its parent, but it
overrides it. It changes the printed text as 'Sand Watch started in
Clock Class.'.

[75]: 1 # Calendar Class


2 class Calendar(object):
3 """Simulates the calendar"""
4
5 def __init__(self, d, m, y):
6 self.set_calendar(d, m, y)
7
8 def set_calendar(self, d, m, y):
9 self.__d = d
10 self.__m = m
11 self.__y = y
12
13 def date(self):
return '{d}:{m}:{y}'.format(d=self.__d,
14
m=self.__m, y=self.__y)
The Calendar class is the same as we defined before. We just
explicitly type the object class as the parent class for it:
Calendar(object) . Which is something that is not mandatory
because every class inherits from the object class either explicitly
or implicitly.

[76]: 1 # CalendarClock Class


2 class CalendarClock(Clock, Calendar):
3 """Keeps calendar and clock together."""
4
def __init__(self, day, month, year, hours, minutes,
5
seconds):
6 # call the super classes init methods
7 Clock.__init__(self, hours, minutes, seconds)
8 Calendar.__init__(self, day, month, year)

In cell 76, you see the definition of the CalendarClock class


which is exactly the same as before. It inherits from two classes as:
CalendarClock(Clock, Calendar). And in the __init__()
method it calls the constructor methods of its parents as:
Clock.__init__(self, hours, minutes, seconds) and
Calendar.__init__(self, day, month, year).
Now that we have all the classes defined let’s create a
CalendarClock object:

[77]: 1 # create a CalendarClock


calender_clock = CalendarClock( 12, 1, 2020, 13, 25,
2
48)

[77]: Sand Watch started in Clock Class.

In cell 77, we instantiate an object from the CalendarClock


class as: calender_clock = CalendarClock(12, 1, 2020, 13, 25,
48) . And it printed as ‘Sand Watch started in Clock Class.’.
Why? Here are the steps of what happened:
Step 1: In the __init__() method of the Clock , we call
super().__init__() which fires the __init__() method of the
SandWatch .
Step 2: In the __init__() method of the SandWatch , we call
the self.start_sand_watch() method.
Step 3: The start_sand_watch() method is overridden inside
the Clock class. So Python calls that message, because it is in
the nearest ancestor.
As you see above, we have the same method name
( start_sand_watch ) in two ancestors of the CalendarClock
class. Both of which print different texts. And the one which is
executed is the nearest one which is the one in the Clock class.
Now let’s call the start_sand_watch() method on our
CalendarClock object one more time to see it clearly:

[78]: 1 # call the start_sand_watch method


2 calender_clock.start_sand_watch()

[78]: Sand Watch started in Clock Class.

As you see in the output of cell 78, when we call the


start_sand_watch() method on the calender_clock object, the
one in the Clock class is executed. Here rises a question.

Question:
If we have the same method in different classes in the
inheritance chain which one will be executed?
Answer:
Python will execute the nearest one, which means the one in the
nearest ancestor.

This rule is called Method Resolution Order (MRO), which is


extremely important when we have multi-level inheritance. You
should always keep it in mind when you call a method that is
implemented on different levels.
The good news is, there is a dunder attribute to get the MRO of
a class. It is called __mro__ . Its syntax is: ClassName.__mro__
and it returns a tuple of orders of classes that will execute first. You
can also call it as the lookup order.

[79]: 1 # MRO for CalendarClock


2 # __mro__
3 print('MRO for CalendarClock:')
4 for m in CalendarClock .__mro__:
5 print(m)

[79]: MRO for CalendarClock:


<class '_5_mro.CalendarClock'>
<class '_5_mro.Clock'>
<class '_5_mro.SandWatch'>
<class '_5_mro.Calendar'>
<class 'object'>

In cell 79, you see the Method Resolution Order (MRO) for the
CalendarClock class. We get it as: CalendarClock.__mro__ .
It’s a tuple so we iterate over it to print the class names. And in the
output you see the list of classes in order.
This list means that, for the CalendarClock class, Python will
look for a method:
1. First in the class itself: CalendarClock
2. Then in the first ancestor which is the parent: Clock
3. Then in the grandparent: SandWatch
2. In the first ancestor which is the parent: Calendar (number
2. here is not a mistake, it is deliberately written. Because Clock
and Calendar are at the same level.)
4. Finally in the root class: object
To make it crystal clear, please examine the diagram below
carefully, in which MRO for the CalendarClock class is depicted:

Figure 9-3: MRO for the CalendarClock class

In Python, there is also a built-in method for all classes which is


called as mro . Its syntax is ClassName.mro() and it works the
same as __mro__ attribute. For example for the CalendarClock
class we can get the MRO tuple as: CalendarClock.mro() .

[80]: 1 # MRO for CalendarClock


2 # mro()
3 print('MRO for CalendarClock:')
4 for m in CalendarClock .mro():
5 print(m)

[80]: MRO for CalendarClock:


<class '_5_mro.CalendarClock'>
<class '_5_mro.Clock'>
<class '_5_mro.SandWatch'>
<class '_5_mro.Calendar'>
<class 'object'>
Operator Overloading
A class can implement certain operations that are invoked by
special syntax (such as arithmetic operations or subscripting and
slicing) by defining methods with special names. Python allows
classes to define their own behavior with respect to language
operators. This is called operator overloading.
For instance there is a special method that exists in every class
which is called __str__() . Its syntax is: Class.__str__(self) or
simply object.__str__ .
This method is called by the format() and print() functions,
to compute the “informal” or nicely printable string representation
of an object. Its return value is a string object. In simple words,
when you print an object as print(object) , the print() function
calls the __str__() method behind the scenes.
In this section we will learn how we can overload the default
definitions of the built-in methods (operators). There are many of
them in Python. Some examples are: __new__ , __init__ ,
__del__ , __str__ , __format__ , __get__ , __set__ , __eq__ ,
__lt__ and so on. All of these methods come from the object
class, which is the root class of all other classes in Python.
We will create a new Python file with name
_6_operator_overloadig.py . In the main.py file, make sure you
comment out the previous files and import this one as:

[81]: 1 # main.py file


2
3 if __name__ == '__main__':
4 # import _1_oop_basics
5 # import _2_classes_and_objects
6 # import _3_inheritance
7 # import _4_multiple_inheritance
8 # import _5_mro
9 import _6_operator_overloadig

Let’s start by defining a simple class named Point , which


represents a point in 2D space:

[82]: 1 # _6_operator_overloadig.py file


2
3 class Point:
4 """A point in coordinate axis."""
5 def __init__(self, x=0, y=0):
6 self.x = x
7 self.y = y

In cell 82, we have the definition of the Point class. In its


__init__() method it defines two instance attributes as: self.x ,
self.y . These attributes represent the x and y coordinates and their
values are provided by the respective parameters.
Let’s create two Point objects with some values for the x and
y coordinates:

[83]: 1 # Create two Point Objects


2 p1 = Point( 2, 5)
3 p2 = Point( -1, 4)

Now let’s print one of these objects, p1 let’s say. We want to


see what it looks like if we print it before we overload the
__str__() method.

[84]: 1 print('----- before overloading -----')


2 print(p1)
3 # __str__() is String representation of object
4 print(p1.__str__())

[84]: ----- before overloading -----


<_6_operator_overloadig.Point object at
0x0000025DA775D990>
<_6_operator_overloadig.Point object at
0x0000025DA775D990>

As you see in cell 84, we print the object p1 as: print(p1) .


And the result is some technical data about the object which
doesn’t mean too much for the consumer of that object. Remember
that printing an object means calling the __str__() method
implicitly. Also in line 4, we call the __str__() method explicitly
as: p1.__str__() . And the output is exactly the same.
Now let’s define our own string representation for the Point
class. In other words, let’s override the __str__() method
(operator overloading).

[85]: 1 # override __str__() method


2 class Point:
3 """A point in coordinate axis."""
4 def __init__(self, x=0, y=0):
5 self.x = x
6 self.y = y
7
8 # overload __str__()
9 def __str__(self):
return 'This is a point at coordinates: ({0},
10
{1})'.format(self.x, self.y)

In cell 85, we override (overload) the built-in __str__()


method in the Point class. Method overloading is very easy in
Python. All you have to do is to redefine that method in your class.
That’s all.
In line 9, you see the redefinition of the __str__() method as:
def __str__(self). And inside the method body we return a
formatted text that tells what this current Point object looks like.
Here is the code: return 'This is a point at coordinates:
{0}x{1}'.format(self.x, self.y). Now if you print an object of type
Point you will see this nice looking and informative text about the
object. Let’s see:

[86]: 1 # print('----- after overloading -----')


2 p1 = Point( 2, 5)
3 print(p1)
4 print(Point.__str__(p1))

[86]: This is a point at coordinates: (2,5)


This is a point at coordinates: (2,5)

As you see in the output of the cell 86, we get a very nice
output when we print the Point class object. In line 4, you see
another way of calling the __str__() method which is with the
class name as: Point.__str__(p1) .
This finalizes our topic on Object Oriented Programming. We
have covered almost all of the fundamental concepts of OOP in
great detail with lots of examples. I hope you learned it by hearth,
because it is one of most important tools that software developers
should have in their toolkits.
QUIZ - OOP
Now it’s time to solve the quiz for this chapter. You can
download the quiz file, QUIZ_OOP.zip, from the Github
Repository of this book. It is in the folder named 9_OOP. You
should put the quiz file in the OOP project we build in this chapter.
Here are the questions for this chapter:

QUIZ - OOP:

Q1:

Define a class named Student. Student has two attributes:


* name (str)
* number (str)
Create two student objects out of this class.
The objects will be:
1- name: James Bond, number: 007
2- name: Clark Kent, number: 333
Print their names.

Hints:
* __init__

Expected Output:
James Bond
Clark Kent

[1]: 1 # Q 1:
2
3 # --- your solution here ---
4
5 # Create Student Objects
6 std_1 = Student( 'James Bond', '777')
7 std_2 = Student( 'Clark Kent', '333')
8
9 print(std_1.name)
10 print(std_2.name)

[1]: James Bond


Clark Kent

Q2:

The Student class you defined in Q1, has a new attribute.


This attribute is 'courses' and it will keep the list of courses
that the student takes.
Its type is list and it will initialize as an empty list.
There will be a method named 'enroll' to enroll the course.
The student will enroll the course, if it has not enrolled already.
Moreover, there will be a method named 'get_courses' which
will return the course list.
Redefine the Student class based on these information.

Student data:
name: John Doe, number: 1111
courses:
* Python Hands-On
* Machine Learning

Expected Output:
student.get_courses() -> ['Python Hands-On', 'Machine
Learning']

[2]: 1 # Q 2:
2
3 # --- your solution here ---
4
5 # print(student.get_courses())

[2]: ['Python Hands-On', 'Machine Learning']

Q3:

Define a class named Point.


It will represent a point in x-y coordinate space.
And the docstring will be:
'it represents a point in (x,y) coordinates.'
Attributes:
* x (int)
* y (int)
Define __init__() method for this class.
And it has a method named 'distance' which calculates the
distance between two Points.
The first point is the current point (self) and the second will be
the parameter.

Hints:
* to calculate distance (d):
* get the difference between x values (x_diff)
* get the difference between y values (y_diff)
* d = math.sqrt(x_diff**2 + y_diff**2)

Expected Output:
p_1 -> (1, 7)
p_2 -> (4, 3)
dist = p_1.distance(p_2)
print(dist) -> 5.0

[3]: 1 # Q 3:
2
3 # --- your solution here ---
4
5 # create two objects
6 # --- your solution here ---
7
8 # print docstring
9 # --- your solution here ---
10
11 dist = p_1 .distance(p_2)
12 print(dist)

[3]: it represents a point in (x,y) coordinates.


5.0

Q4:

Define a class named Rectangle.


It represents a rectangle on the X-Y coordinate axis.
And this is its docstring:
"This class represents a rectangle on (x,y) coordinate axis."
Rectangle has 4 attributes:
* corner_1 (Point)
* corner_2 (Point)
* corner_3 (Point)
* corner_4 (Point)
The type of these 4 corners are the Point class we defined in Q3.
Corner 1 and 2 are on the same line (side).
Corner 3 and 4 are on the same line (side).
This is how the corners look like:
1..............2
. .
. .
. .
3..............4
Define the __init__() method for the Rectangle class.
It has methods to calculate its width and length.
* width = distance between corner 1 and 2 ->
calculate_width()
* length = distance between corner 1 and 3 ->
calculate_length()
Rectangle uses Point class to calculate these distances.
Finally, Rectangle has a method which returns its area.
The name of this method is 'area'.

Expected Output:
p_1 = Point(5, 8)
p_2 = Point(9, 8)
p_3 = Point(5, 2)
p_4 = Point(9, 2)

width = rect.width
length = rect.length
area = rect.area()

width: 4.0
length: 6.0
area: 24.0

[4]: 1 # Q 4:
2
3 # --- your solution here ---
4
5 # create Point objects
6 # --- your solution here ---
7
8 # create Rectangle object
9 # --- your solution here ---
10
11 # Print width, length and area
12 # --- your solution here ---
[4]: width: 4.0
length: 6.0
area: 24.0

Q5:

Define a class named BankAccount.


It has an attribute: balance (int)
And it has two methods:
* withdraw -> removes money from balance
* deposit -> adds money to balance
Both methods will update the balance and return the final
balance.
The initial balance is 0. (__init__())
Finally there is method named 'display_balance' and it will
print the current balance as:
'Balance: xxxxxx'

Expected Output:
account = BankAccount()
account.display_balance()
account.deposit(500)
account.display_balance()
account.withdraw(200)
account.display_balance()

Balance: 0
Balance: 500
Balance: 300

[5]: 1 # Q 5:
2
3 # --- your solution here ---
4
5 # create object and call methods
6 account = BankAccount()
7 account.display_balance()
8
9 account.deposit(500)
10 account.display_balance()
11
12 account.withdraw(200)
13 account.display_balance()

[5]: Balance: 0
Balance: 500
Balance: 300

Q6:

Define a class named MinimumBankAccount.


This class will inherit from the BankAccount defined in Q5.
It will have an attribute as 'min_balance' to keep the minimum
balance.
The value for min_balance will be defined while object
instantiation. (__init__())

We will check the min_balance when the user wants to


withdraw:
* If after withdraw, the balance is less than min_balance
* we will not let this withdraw
* and we will return as: 'Sorry, you cannot withdraw this
amount.'
Finally modify the class so that it prints some meaningful data
like:
print(MinimumBankAccount) -> 'This is MinimumBankAccount
class'.

Hints:
* super()
* __str__ -> operator overloading
* do not implement withdraw in this class.
Super class (BankAccount) is already doing this.
You should just check it here.

Expected Output:
min_account = MinimumBankAccount(100)
print(min_account)
min_account.deposit(500)
min_account.display_balance()
min_account.withdraw(200)
min_account.display_balance()
min_account.withdraw(300)
min_account.display_balance()

This is MinimumBankAccount class


Balance: 500
Balance: 300
Sorry, you cannot withdraw this amount.
Balance: 300

[6]: 1 # Q 6:
2
3 # --- your solution here ---
4
5 # create an object
6 min_account = MinimumBankAccount( 100)
7
8 # print the class
9 print(min_account)
10
11 min_account.deposit(500)
12 min_account.display_balance()
13
14 min_account.withdraw(200)
15 min_account.display_balance()
16
17 # withdraw the remaining amount
18 min_account.withdraw(300)
19 min_account.display_balance()

[6]: This is MinimumBankAccount class.


Balance: 500
Balance: 300
Sorry, you can not withdraw this amount.
Balance: 300

Q7:

What are the results of the print() functions below?

class K:
def a(self):
return self.b()

def b(self):
return 'K'

class L(K):
def b(self):
return 'L'

k = K()
l = L()

print(k.b(), l.b())
print(k.a(), l.a())
Q8:

Define two classes as below:


Song:
The class for songs.
Each song has: 'name', 'artist', 'album' and 'song_number'.
'album' will be of type Album class.
Album:
The class for albums.
Each album has: 'name', 'artist', 'year' and 'songs'.
'songs' will be a list of Song class objects.
Album class has a method named 'add_song' to append a song
to the 'songs' list.
It creates a song number while appending to the list.
Print the songs in the Album.

Expected Output:
album = Album('Yesterday and Today', 'The Beatles', 1966)
album.add_song('Yesterday')
album.add_song('Act Naturally')
album.add_song('What Goes On')

Yesterday
Act Naturally
What Goes On

[8]: 1 # Q 8:
2
3 # --- your solution here ---
4
5 # create an Album
6 # --- your solution here ---
7
8 # add songs
9 # --- your solution here ---
10
11 # print the songs with a for loop
12 # --- your solution here ---

[8]: Yesterday
Act Naturally
What Goes On

Q9:

Define a class named Artist.


Each artist has 'name' and 'albums'.
'albums' is a list of type Album class we defined in Q8.
Methods:
* 'add_album(Album)' -> adds an album
Print all the songs of the artist in two ways.
1. with the for loop
2. with comprehension

Expected Output:
# Artist
beatles = Artist('The Beatles')

# Album
album_1 = Album('Yesterday and Today', 'The Beatles', 1966)
album_1.add_song('Yesterday')
album_1.add_song('Act Naturally')
beatles.add_album(album_1)

album_2 = Album('Let It Be', 'The Beatles', 1970)


album_2.add_song('Let It Be')
album_2.add_song('For You Blue')
beatles.add_album(album_2)
----- albums -----
Yesterday and Today
Let It Be
---- songs ------
Yesterday
Act Naturally
Let It Be
For You Blue
----- songs - comprehension -----
Yesterday
Act Naturally
Let It Be
For You Blue

[9]: 1 # Q 9:
2
3 # --- your solution here ---
4
5 # Artist
6 beatles = Artist( 'The Beatles')
7
8 # Album
album_1 = Album( 'Yesterday and Today', 'The
9
Beatles', 1966)
10 album_1.add_song('Yesterday')
11 album_1.add_song('Act Naturally')
12 beatles.add_album(album_1)
13
14 album_2 = Album( 'Let It Be', 'The Beatles', 1970)
15 album_2.add_song('Let It Be')
16 album_2.add_song('For You Blue')
17 beatles.add_album(album_2)
18
19 print('----- albums -----')
20 # --- your solution here ---
21
22
23 print('---- songs ------')
24 # --- your solution here ---
25
26
27 print('----- songs - comprehension -----')
28 # --- your solution here ---

[9]: ----- albums -----


Yesterday and Today
Let It Be
---- songs ------
Yesterday
Act Naturally
Let It Be
For You Blue
----- songs - comprehension -----
Yesterday
Act Naturally
Let It Be
For You Blue

Q10:

Overload the add (+) operator for Album class.


You will be able to add to Albums as:
Album + Album
And the result of this addition is the list of all songs in both
classes.
If you do not overload the + operator and call as:
print(album_1 + album_2)
You will get an error:
TypeError: unsupported operand type(s) for +: 'Album' and
'Album'

Hints:
* __add__() -> this is + operator method

Expected Output:
album_1 = Album('Yesterday and Today', 'The Beatles', 1966)
album_1.add_song('Yesterday')
album_1.add_song('Act Naturally')

album_2 = Album('Let It Be', 'The Beatles', 1970)


album_2.add_song('Let It Be')
album_2.add_song('For You Blue')

all_songs = album_1 + album_2

Yesterday
Act Naturally
Let It Be
For You Blue

[10]: 1 # Q 10:
2
3 # --- your solution here ---
4
5 # create albums and add songs
6 # --- your solution here ---
7
8 # add two albums
9 all_songs = album_1 + album_2
10
11 # print all songs in both albums
12 # --- your solution here ---
[10]: Yesterday
Act Naturally
Let It Be
For You Blue
SOLUTIONS - OOP
Here are the solutions for the quiz for Chapter 9 - OOP.

SOLUTIONS - OOP:

S1:

[1]: 1 # S 1:
2 class Student:
3
4 def __init__(self, name, number):
5 self.name = name
6 self.number = number
7
8 # Create Student Objects
9 std_1 = Student( 'James Bond', '777')
10 std_2 = Student( 'Clark Kent', '333')
11
12 print(std_1.name)
13 print(std_2.name)

[1]: James Bond


Clark Kent

S2:

[2]: 1 # S 2:
2 class Student:
3
4 def __init__(self, name, number):
5 self.name = name
6 self.number = number
7 # list of courses
8 self.courses = []
9
10 def enroll(self, course):
11 # check if already enrolled
12 if not course in self.courses:
13 self.courses.append(course)
14
15 def get_courses(self):
16 return self.courses
17
18
19 # create object
20 student = Student( 'John Doe', '1111')
21
22 student.enroll('Python Hands-On')
23 student.enroll('Machine Learning')
24
25 print(student.get_courses())

[2]: ['Python Hands-On', 'Machine Learning']

S3:

[3]: 1 # S 3:
2 import math
3
4 class Point:
5 """it represents a point in (x,y) coordinates."""
6
7 def __init__(self, x, y):
8 self.x = x
9 self.y = y
10
11 def distance(self, other):
12 x_diff = self.x - other .x
13 y_diff = self.y - other .y
14 d = math .sqrt(x_diff**2 + y_diff **2)
15 return d
16
17 # create two objects
18 p_1 = Point( 1, 7)
19 p_2 = Point( 4, 3)
20
21 # print docstring
22 print(Point.__doc__)
23
24 dist = p_1 .distance(p_2)
25 print(dist)

[3]: it represents a point in (x,y) coordinates.


5.0

S4:

[4]: 1 # S 4:
2 class Rectangle:
"This class represents a rectangle on (x,y)
3
coordinate axis."
4 def __init__(self, p_1, p_2, p_3, p_4):
5 self.corner_1 = p_1
6 self.corner_2 = p_2
7 self.corner_3 = p_3
8 self.corner_4 = p_4
9 self.calculate_width()
10 self.calculate_length()
11
12 def calculate_width(self):
self.width =
13
self.corner_1.distance(self.corner_2)
14
15 def calculate_length(self):
self.length =
16
self.corner_1.distance(self.corner_3)
17
18 def area(self):
19 return self.width * self.length
20
21 # create Point objects
22 p_1 = Point( 5, 8)
23 p_2 = Point( 9, 8)
24 p_3 = Point( 5, 2)
25 p_4 = Point( 9, 2)
26
27 # create Rectangle object
28 rectangle = Rectangle(p_1, p_2, p_3, p_4)
29
30 # Print width, length and area
31 print('width:', rectangle.width)
32 print('length:', rectangle.length)
33 print('area:', rectangle.area())

[4]: width: 4.0


length: 6.0
area: 24.0

S5:
[5]: 1 # S 5:
2 class BankAccount:
3
4 def __init__(self):
5 self.balance = 0
6
7 def withdraw(self, amount):
8 self.balance -= amount
9 return self.balance
10
11 def deposit(self, amount):
12 self.balance += amount
13 return self.balance
14
15 def display_balance(self):
16 print('Balance:', self.balance)
17
18 # create object and call methods
19 account = BankAccount()
20 account.display_balance()
21
22 account.deposit(500)
23 account.display_balance()
24
25 account.withdraw(200)
26 account.display_balance()

[5]: Balance: 0
Balance: 500
Balance: 300

S6:
[6]: 1 # S 6:
2 class MinimumBankAccount(BankAccount):
3 def __init__(self, min_balance):
4 super().__init__()
5 self.min_balance = min_balance
6
7 # override withdraw
8 def withdraw(self, amount):
9 # check when withdraw
10 if self.balance - amount < self.min_balance:
print('Sorry, you can not withdraw this
11
amount.')
12 else:
13 BankAccount.withdraw(self, amount)
14
15 # __str__() -> overload -> print()
16 def __str__(self):
17 return 'This is MinimumBankAccount class.'
18
19 # create an object
20 min_account = MinimumBankAccount( 100)
21
22 # print the class
23 print(min_account)
24
25 min_account.deposit(500)
26 min_account.display_balance()
27
28 min_account.withdraw(200)
29 min_account.display_balance()
30
31 # withdraw the remaining amount
32 min_account.withdraw(300)
33 min_account.display_balance()

[6]: This is MinimumBankAccount class.


Balance: 500
Balance: 300
Sorry, you can not withdraw this amount.
Balance: 300

S7:

[7]: 1 # S 7:
2 class K:
3 def a(self):
4 return self.b()
5
6 def b(self):
7 return 'K'
8
9 class L(K):
10 def b(self):
11 return 'L'
12
13
14 k = K()
15 l = L()
16
17 print(k.b(), l.b())
18 # K, L
19
20 print(k.a(), l.a())
21 # K, L
[7]: K L
K L

S8:

[8]: 1 # S 8:
2 class Song:
def __init__(self, name, artist, album,
3
song_number):
4 self.name = name
5 self.artist = artist
6 self.album = album
7 self.song_number = song_number
8
9 def __str__(self):
10 return self.name
11
12 class Album:
13 def __init__(self, name, artist, year):
14 self.name = name
15 self.artist = artist
16 self.year = year
17 self.songs = []
18
19 def add_song(self, song_name):
20 # length of songs
21 song_number = len(self.songs)
22
23 # create Song object
song = Song(song_name, self.artist, self,
24
song_number)
25
26 # append this song into songs
27 self.songs.append(song)
28
29 def __str__(self):
30 return self.name
31
32 # create an Album
album = Album( 'Yesterday and Today', 'The Beatles',
33
1966)
34
35 # add songs
36 album.add_song('Yesterday')
37 album.add_song('Act Naturally')
38 album.add_song('What Goes On')
39
40 # print the songs with a for loop
41 for song in album .songs:
42 print(song)

[8]: Yesterday
Act Naturally
What Goes On

S9:

[9]: 1 # S 9:
2 class Artist:
3 def __init__(self, name):
4 self.name = name
5 self.albums = []
6
7 def add_album(self, album):
8 if not album in self.albums:
9 self.albums.append(album)
10
11 # Artist
12 beatles = Artist( 'The Beatles')
13
14 # Album
album_1 = Album( 'Yesterday and Today', 'The
15
Beatles', 1966)
16 album_1.add_song('Yesterday')
17 album_1.add_song('Act Naturally')
18 beatles.add_album(album_1)
19
20 album_2 = Album( 'Let It Be', 'The Beatles', 1970)
21 album_2.add_song('Let It Be')
22 album_2.add_song('For You Blue')
23 beatles.add_album(album_2)
24
25 print('----- albums -----')
26 for album in beatles .albums:
27 print(album)
28
29 print('---- songs ------')
30 for album in beatles .albums:
31 for song in album .songs:
32 print(song)
33
34 print('----- songs - comprehension -----')
35 [print(song)
36 for album in beatles .albums
37 for song in album .songs]

[9]: ----- albums -----


Yesterday and Today
Let It Be
---- songs ------
Yesterday
Act Naturally
Let It Be
For You Blue
----- songs - comprehension -----
Yesterday
Act Naturally
Let It Be
For You Blue

S10:

[10]: 1 # S 10:
2 class Album(object):
3 def __init__(self, name, artist, year):
4 self.name = name
5 self.artist = artist
6 self.year = year
7 self.songs = []
8
9 def add_song(self, song_name):
10 # length of songs
11 song_number = len(self.songs)
12
13 # create Song object
song = Song(song_name, self.artist, self,
14
song_number)
15
16 # append this song into songs
17 self.songs.append(song)
18
19 def __str__(self):
20 return self.name
21
22 # overload -> + operator -> __add__() method
23 def __add__(self, other):
24 all_songs = self.songs + other .songs
25 return all_songs
26
27 # create albums and add songs
album_1 = Album( 'Yesterday and Today', 'The
28
Beatles', 1966)
29 album_1.add_song('Yesterday')
30 album_1.add_song('Act Naturally')
31
32 album_2 = Album( 'Let It Be', 'The Beatles', 1970)
33 album_2.add_song('Let It Be')
34 album_2.add_song('For You Blue')
35
36 # add two albums
37 all_songs = album_1 + album_2
38
39 # print all songs in both albums
40 for song in all_songs:
41 print(song)

[10]: Yesterday
Act Naturally
Let It Be
For You Blue
10. Project 2 - Movie Library with Tkinter
In this project we will create a GUI (Graphical User Interface)
application with Python. This is our last project and we will create
a Movie Library which replicates the IMDB Top 250 Movies list.
We will use almost all the concepts we covered so far and this will
give you the chance to practice what you learned in this book. Our
main focus will be on Object Oriented Programming and classes as
you may expect.

The Mockups
This will be a big project with lots of modules in it, and at the
end you will have a fully working app, which you can continue to
develop. Here are the mockups of our project:
Figure 10-1: Home Page

In Figure 10-1 you see the Home Page. This is the initial page
for our Movie Library. On the left you see the menu and on the
right there is the content area. In the menu, we have four pages as
Home, Movie List, Movie Detail and About. We will develop the
first three pages in this project. You can continue with more pages
if you want.
Figure 10-2: Movie List

In Figure 10-2 you see the Movie List page. This page contains
the list of IMDB Top 250 movies. It is paginated and we have 10
movies on each page. We have the small image on the first column
of each row. This images are provided in the project directory. The
column headers are Row Number (#), Title, Year, Rating and # of
Ratings. You can click on any row to see the details of that movie.
It will navigate you to the Movie Detail page. The color of the
selected button on the left menu becomes orange when we
navigate.
Figure 10-3: Movie Detail

On the Movie Detail Page, we have all the data about the
selected movie: Id, Title, Year, Runtime, Genre, Director, Actors,
Language, Country, Awards, imdbRating, imdbVotes and imdbID.
On the top of the page we have the Title and the big image of the
movie. Big images are provided in the project directory.

We will create a new PyCharm project. The project name is


Movie_Library. You should download the project from the Github
Repository of the book before starting to code. Because there are
lots of files that you will need while developing this project. We
will also install some third party Python packages which are
necessary for the project.
Here is the project that we will develop in this chapter:

Figure 10-4: Movie_Library project for this chapter

As you see in Figure 10-4 we have organized the project in


different directories and packages. We have a main.py file which
will be the entry point for our Movie Library.

Here are the folders and packages which we will create in this
project:
1. data : it will contain data files like colors.py, geometry.py,
imdb_top_250.csv and menus.py
2. images : it will contain folders like home, posters_large and
posters_small for movie images
3. mockup : it will contain the mockup images of the project
4. page : it is a package that contains the page folders as home,
movie_detail and movie_list
5. tkinter_basics : it contains basic widgets to learn Tkinter
6. widget : it is a package that contains the widget folders as
button, left_frame, right_frame and window

This chapter includes two main parts: Tkinter Basics and


Movie Library itself. We will start by learning the basics of
Tkinter package and GUI programming in Python. Then we will
develop our project. Let’s start with Tkinter Basics.
Tkinter Basics
The Tkinter package (also known as “Tk interface”) is the GUI
framework which is built into the Python standard library. It is
lightweight and cross-platform, which means the same code works
on Windows, macOS, and Linux. Although there are more
powerful and modern third-party toolkits for Python GUI
programming (like PyQt or Kivy), Tkinter is suitable for
educational purposes. And since it is built-in, you do not need a
separate installation to use it. Once you understand how Tkinter
works, then it will become very easy to use any other GUI
framework.
In our project folder, we will create a new directory for this
section. Let’s name it as tkinter_basics . This is not a core part in
our Movie Library project. We will only use it to see some basic
concepts of Tkinter.
Inside the tkinter_basics folder create a new Python file as
tkinter_intro.py . We will use it to learn some initial concepts
about Tkinter.

Life Cycle of a Tkinter App:


Tkinter apps have a typical life cycle as follows:
i. You import Tkinter package as: import tkinter
ii. You create a Main Window (entry point)
iii. You create your Widgets (button, text, label, frame etc.)
iv. You have the mainloop to keep the app alive

Let’s write these steps in our tkinter_intro.py file and start


coding in Tkinter:

[1]: 1 # tkinter_intro.py file


2
3 import tkinter as tk
4
5 # main window
6 main_window = tk .Tk()
7
8 #
# WIDGETS -> Controls (Buttons, Text, Label,
9
Frame ...)
10 #
11
12 # last line -> mainloop
13 main_window.mainloop()

In cell 1, we have the tkinter_intro.py file content.


In line 3, we import the Tkinter package as: import tkinter as
tk . Which means, in this file we will call Tkinter as tk .
In line 6, we create our main window as: main_window =
tk.Tk() . Tkinter has a class called Tk that creates a top level
widget of Tk which usually is the main window (the root) of an
application. We call that class and name the resulting object as
main_window .
In lines between 7 and 11 we will place our widgets. We don’t
have any for the time being, but will have in a minute.
And finally in line 13, we have the mainloop method as:
main_window.mainloop() . The mainloop() method puts
everything on the display, and responds to user input until the
program terminates. This line must be always the last line a Tkinter
app. It keeps the app running, until you close the running window.
Now let’s run this code and see our first Tkinter window on the
screen. In PyCharm, right click anywhere on the code editor and
select Run ‘tkinter_intro’. You should a see a window like the
one below:

Figure 10-5: First Tkinter window

If you see an empty Tkinter window like the one in Figure 10-
5, then you are all set. Your Tkinter application is up and running.
We will fill it with widgets soon.

Geometry Managers for Widget Placement:


In Tkinter, you need to ‘place’ a widget to be able to see it on
the screen. There are three geometry managers for widget
placement which are:
1. pack() : packs the widgets one after another (from top to
bottom or from left to right). It has options like fill , expand , and
side to control the placement.
2. grid() : puts the widgets in a grid which is a 2-dimensional
table (row & column base)
3. place() : puts the widgets in exact (absolute) locations on the
window (x=100, y=300 text units)

Now we are ready to create some widgets and see them on our
Tkinter window. Let’s start with a Button .
Button
Inside the tkinter_basics folder create a new Python file as
button.py . We will create a Tkinter Button in this file. Here is the
complete code of the file:

[2]: 1 # button.py file


2
3 # import
4 import tkinter as tk
5
6 # main window
7 window = tk .Tk()
8 window.title('Tkinter Basics - Button')
9 window.geometry('600x400') # width x height
10
11 # Button Widget
12 btn = tk .Button(master=window,
13 text='Close',
# text-unit -> width (width of one char of
14
text)
15 width=45,
16 # height (height of one char of text)
17 height=10,
18 bg='red',
19 fg='white',
20 # what to do when clicked
21 command=window.destroy)
22
23 # place the button
24 btn.pack()
25
26 # last line -> mainloop
27 window.mainloop()
In cell 2, we have the complete code in the button.py file. And
you also see the Tkinter life cycle here.
In line 4, we start by importing the Tkinter package as: import
tkinter as tk.
In line 7, we create a main window as: window = tk.Tk(). In
line 8, we set the title for our window as: window.title('Tkinter
Basics - Button'). And in line 9, we set the width and height of
our window as: window.geometry('600x400') .
In line 12, we create a button, which we name as btn . The
button class is tk.Button() , and it has some parameters which
allows us the customize the functionality and look & feel of the
button.
Here are the parameters we use in this example:
1. master : this represents the parent widget, which is the main
window in our case: master=window
2. text : the text to display on the button
3. width & height: the width and height of the button (in text
units)
4. bg : background color of the button
5. fg : foreground (text) color of the button
6. command : function or method to be called when the button is
clicked. We call window.destroy for this button.
Now we have a button created but it is not enough to see it on
the screen. We need to pack it to be able to make it visible. And
that’s what we do in line 24 as: btn.pack() . Since we set the
window object as the master of our button ( master=window ),
Python will put it in the window when we call the pack() method.
And as stated before, the last line is the mainloop as:
window.mainloop() .
Now we can run this file to see our fancy button. In PyCharm,
right click anywhere on the code editor and select Run ‘button’.
You should a see a window like the one below:

Figure 10-6: Tkinter Basics - Button window (cropped)

If you see the image in Figure 10-6, then you’re button code
works correct. If you click on the button it will close the window,
because we pass window.destroy for the command parameter.

Checkbutton
Inside the tkinter_basics folder create a new Python file as
checkButton.py . We will create a Tkinter Checkbutton in this
file. Here is the complete code of the file:

[3]: 1 # checkButton.py file


2
3 # import
4 import tkinter as tk
5
6 # main window
7 window = tk .Tk()
8 window.title('Tkinter Basics - Checkbutton')
9 window.geometry('600x400') # width x height
10
11 # Checkbutton widget
chkBtn_1 = tk .Checkbutton(master=window,
12
text='Correct')
chkBtn_2 = tk .Checkbutton(master=window,
13
text='Incorrect')
14
15 # place the check button
16 chkBtn_1.grid(row=0, column=0)
17 chkBtn_2.grid(row=1, column=0)
18
19 # last line -> mainloop
20 window.mainloop()

In cell 3, you see the code in the checkButton.py file. The


lines from 4 to 9 are the same as we did with the Button in cell 2.
We import Tkinter package as tk , then create the main window,
set the title and geometry for the window .
In line 12 we create our first Checkbutton as chkBtn_1 . Here
is the code for it: chkBtn_1 = tk.Checkbutton(master=window,
text='Correct') .
The parameters are:
1. master : this represents the parent widget, which is the main
window in our case: master=window
2. text : the text to display on the check button
And in line 13, we create the second Checkbutton as
chkBtn_2 . The only difference with chkBtn_1 is the text it
displays.
In lines 16 and 17 we place the check buttons on the screen. We
use the grid() method this time:
To place the first one we use: chkBtn_1.grid(row=0,
column=0) . If you think the window as a grid, chkBtn_1 is
placed on the first row ( row=0 ) and first column
( column=0 ).
To place the second one we use: chkBtn_2.grid(row=1,
column=0) . If you think the window as a grid, chkBtn_2 is
placed on the second row ( row=1 ) and first column
( column=0 ). Which means chkBtn_1 and chkBtn_2 will be
placed vertically.
And the last line is the mainloop as: window.mainloop() .
Now we can run this file to see the check buttons. In PyCharm,
right click anywhere on the code editor and select Run
‘checkButton’ . You should a see a window like the one below:

Figure 10-7: Tkinter Basics – Checkbutton window (cropped)

Label & Entry


In Tkinter, Label is a widget that is used to implement display
boxes where you can place text or images. And Entry is a widget
that is used to accept single-line text strings from the user.
Inside the tkinter_basics folder create a new Python file as
label_entry.py . We will create Tkinter Label and Entry widgets
in this file. Here is the complete code of the file:

[4]: 1 # label_entry.py file


2
3 # import
4 import tkinter as tk
5 from tkinter import Label, Entry
6
7 # Label -> read only text on screen
8 # Entry -> one line text box
9
10 # main window
11 window = tk .Tk()
12 window.title('Tkinter Basics - Label - Entry')
13 window.geometry('600x400') # width x height
14
15 # Label Widgets
16 lblName = Label(master =window, text='Name')
17 lblLastName = Label(window, text='Last Name')
18
19 # Entry Widgets
20 entName = Entry(window)
21 entLastName = Entry(window)
22
23 # place the label - entry
24 lblName.grid(row=0, column=0)
25 entName.grid(row=0, column=1)
26
27 lblLastName.grid(row=1)
28 entLastName.grid(row=1, column=1)
29
30 # last line -> mainloop
31 window.mainloop()

In cell 4, you see the code in the label_entry.py file. This time
we import the classes with named imports as: from tkinter
import Label, Entry. Now we can directly use the class names to
create widgets.
In lines 16 and 17 we create two Label widgets. We gave the
names of lblName and lblLastName to them. And here is the
code for the first one: lblName = Label(master=window,
text='Name') .
In lines 20 and 21 we instantiate two Entry widgets as
entName and entLastName . The only parameter we use is the
master , which we pass the window as: entName =
Entry(window) .
In lines 24 and 25 we place the first Label and the first Entry .
Since we want them to be on the same horizontal line, we set their
row numbers equal ( row=0 ). Here is the code for the Label:
lblName.grid(row=0, column=0). And the code the Entry widget:
entName.grid(row=0, column=1). Be careful that, the column
number for the entName is 1 .
In lines 27 and 28 we place the second Label and the second
Entry on the same horizontal line ( row=1 ). Here is the code for
the Label widget: lblLastName.grid(row=1) . We skip the
column parameter for lblLastName because the default value is
0 . And the code the Entry widget: entLastName.grid(row=1,
column=1) . Be careful that, the column number for the
entLastName is 1 .
And the last line is the mainloop as: window.mainloop() .
Now we can run this file to see the labels and entries. In
PyCharm, right click anywhere on the code editor and select Run
‘label_entry’ . You should a see a window like the one below:

Figure 10-8: Tkinter Basics - Label – Entry window (cropped)

Frame
Frame is a widget that is used for grouping and organizing
other widgets. It works like a container, which is responsible for
arranging and positioning its content. It is a rectangular region on
the screen that organizes the layout and provides padding to other
widgets.
Inside the tkinter_basics folder create a new Python file as
frame.py . We will create a Tkinter Frame widget in this file.
Here is the complete code of the file:

[5]: 1 # frame.py file


2
3 # import
4 import tkinter as tk
5 from tkinter import Frame, Button
6
7 # Frame: container that holds other widgets
8
9 # main window
10 window = tk .Tk()
11 window.title('Tkinter Basics - Frame')
12 window.geometry('600x400') # width x height
13
14 # Frame Widget
15 frame = Frame(window)
16
17 # Buttons
btnLeft = Button(master =frame, text='Frame Button
18
Left', bg='black', fg='white')
19 btnLeft.pack(side=tk.LEFT)
20
btnRight = Button(master =frame, text='Frame
21
Button Right', bg='black', fg='white')
22 btnRight.pack(side=tk.RIGHT)
23
24 # place the widgets
25 frame.pack()
26
27 # last line -> mainloop
28 window.mainloop()

In cell 5, you see the code in the frame.py file. We import the
classes with named imports as: from tkinter import Frame,
Button .
In line 15, we create a Frame object as: frame =
Frame(window) . As you see we only pass the first parameter
which is the master .
In line 18, we create a button as btnLeft . Here is the code:
btnLeft = Button(master=frame, text='Frame Button Left',
bg='black', fg='white'). Please be careful that, this time the
master of the button is not window. The master is our frame ,
because we want the button to be in the frame: master=frame .
And in line 19, we pack this button as:
btnLeft.pack(side=tk.LEFT) . The side parameter decides where
to put the button in its parent container. And we pass
side=tk.LEFT . Which means it will be placed on the left.
In lines 21 and 22, we instantiate another button as btnRight
in the same way we created the previous one. The only difference
here is the placement. We pack this one to the right as:
btnRight.pack(side=tk.RIGHT) .
In line 25, we pack the frame as : frame.pack(). Since its
master is the window , Python will put it inside the window
object.
And the last line is the mainloop as: window.mainloop() .
Now we can run this file to see the two buttons in a frame. In
PyCharm, right click anywhere on the code editor and select Run
‘frame’ . You should a see a window like the one below:

Figure 10-9: Tkinter Basics – Frame window (cropped)

Text
The Text widget is used where we need to insert multi-line text
fields. It allows us to display and edit multi-line text area and
format it, such as changing its color and font.
Inside the tkinter_basics folder create a new Python file as
text.py . We will create a Tkinter Text widget in this file. Here is
the complete code of the file:

[6]: 1 # text.py file


2
3 # import
4 import tkinter as tk
5 from tkinter import Text
6
7 # Text: multi line text input (text box)
8
9 # main window
10 window = tk .Tk()
11 window.title('Tkinter Basics - Text')
12 window.geometry('600x400') # width x height
13
14 # Text Widget (Text Area)
15 txt = Text(window, height=5, width=40)
16
17 # place the widget
18 txt.pack()
19
20 # last line -> mainloop
21 window.mainloop()

In cell 6, you see the code in the text.py file. We import the
Text class with named imports as: from tkinter import Text.
In line 15, we create a Text widget as: txt = Text(window,
height=5, width=40). For the first parameter, which is master ,
we pass our main window object. Then we pass the height and
width for the text area.
In line 18, we pack our txt object as: txt.pack() .
And the last line is the mainloop as: window.mainloop() .
Now we can run this file to see a multi-line text on the screen.
In PyCharm, right click anywhere on the code editor and select
Run ‘text’. You should a see a window like the one below:

Figure 10-10: Tkinter Basics – Text window (cropped)

This finalizes our section on Tkinter Basics. Now you are


familiar with Tkinter and its widget structure. We can start building
our Movie Library project in the next section.
Movie Library
Our Movie Library is composed of different modules and
packages. We will have two separate packages as page and
widget . In the page package we will keep our page objects. And
in the widget package we will define the widgets. We will have a
data directory to keep common data objects and files like; colors,
menus, geometry and IMBD Top 250 movies list. Don’t worry, we
will create these folders and packages one by one.

Main Window Structure:


Let’s talk a little bit on the structure of our main window. First
let’s examine the image below:

Figure 10-11: Main Structure of the project

As you see, in our Main Window we have two main sections


as: Left Frame and Right Frame. These are the main containers
for the widgets we will create. We will create classes with these
names, since we want everything to be object oriented in our
project.

The data Folder


Before creating our frame classes, let’s create the data folder
(if you haven’t already). In PyCharm, right click on the project
name and select New > Directory. Name the directory as data
and hit Enter.
Here are the files in this folder: colors.py , geometry.py ,
imdb_top_250.csv and menus.py . We will use these files in
many places in our code, so it is a good idea to keep them in a
central directory where anyone can access. You can find the
imdb_top_250.csv file in the project folder in the Github
Repository of the book.

menus.py :
This file contains a dictionary that stores the menu names and
texts. Here is the content of it:

[7]: 1 # menus.py file


2
3 MENU = {
4 'home': 'Home',
5 'movieList': 'Movie List',
6 'movieDetail': 'Movie Detail',
7 'about': 'About',
8 }

In cell 7, you see definition of the MENU dictionary. The keys


are menu names and the values are the text that we will display on
the screen. We will be needing this dictionary when we create the
menu buttons in the Left Frame.
geometry.py :
This file contains the geometry elements for the main window
as width and height. We define width and height as constants. Here
is the code in this file:

[8]: 1 # geometry.py file


2
3 class GEOMETRY:
4 """Class for Geometry constants."""
5
6 MAIN_WINDOW_WIDTH = 960
7 MAIN_WINDOW_HEIGHT = 880

As you see in cell 8, we define a class named GEOMETRY .


And it has two constants (class attributes) in it. As a convention,
capital letters mean constants in many programming language. So
we define the class name and attribute names in full capitals. The
attributes are MAIN_WINDOW_WIDTH and
MAIN_WINDOW_HEIGHT . Whenever we need to use the main
window width we can just call it as: GEOMETRY.
MAIN_WINDOW_WIDTH .

colors.py :
The last Python file in the data folder is colors.py . In which
we define a simple class called COLORS . This class will store the
constants for the colors we will use throughout the project. Here is
its content:

[9]: 1 # colors.py file


2
3 class COLORS:
4 """
5 Class for Theme colors.
6 """
7
8 # Class Attributes
9 BLACK = '#121212'
10 GRAY = '#686868'
11 ORANGE = 'orange'
12 WHITE = '#ffffff'
13 LIGHT_BLUE = '#5799ef'
14 LIST_ODD_LINE = '#f6f6f5'
15 LIST_EVEN_LINE = '#fbfbfb'

In cell 9, you see the definition of the COLORS class. We


have constants (class attributes) for color codes that we will use for
designing the project theme.

main.py
In most of the Python projects, the entry point is main.py file.
This is where you start the project, create necessary objects and call
related functions to run the project. We will stick to this convention
here, so main.py file is the entry point of our Movie Library.
In our main.py file we will set the structure what you see in
Figure 10-11. We will create the Main Window first, then the
Left Frame and finally the Right Frame. Here is the code:

[10]: 1 # main.py file


2
3 """
4 This is the main file of Movie Library:
5 Pages:
6 1- Home
7 2- Movie List
8 3- Movie Detail
9 https://www.imdb.com/chart/top/
10 """
11
12 # import window and frames
13
14 if __name__ == '__main__':
15 # Root Window
16
17 # LEFT FRAME
18
19 # RIGHT FRAME
20
21 # start root window -> mainloop()

In cell 10, you see the initial form of our main.py file. It has
no functionality yet. It only has some placeholder comments for the
time being. But soon we will fill it with our classes.
It starts by importing the classes for the main window and
frames. As you see in the structure in Figure 10-11, there are three
parts in the main.py file which are:
1. Root Window which is the main window and the class for it is:
Window
2. Left Frame is left side of the screen and the class is:
LeftFrame
3. Right Frame is right hand side and the class is: RightFrame

And finally we will have a method to call the mainloop() to


start our application and keep it running.
This is the initial state of the main.py file, but we will revisit it
as soon as we define necessary classes.
Packages
We want our Movie Library project to be modular as much as
possible. This is the key for scalability. If you have a good
organization of your files and folders, then no matter the size of
your project, you can still manage it without trouble. When your
project size becomes too big, a proper organization may save your
life. Or it may be just the opposite with a bad organization.
We will define two packages in our project which are: widget
and page . Let’s create them:

widget Package:
We will create a Python package for our widgets. We want all
of our widgets to be located in the same directory, which will make
things easier when anyone needs to use them. Each widget will be
in a separate folder. And in each folder, there will be a class for
that widget. Here are the folders for widgets: button , left_frame ,
right_frame and window .
To create the widget package, in PyCharm right click on the
project name and select New > Python Package. Name the
package as widget and hit Enter. Now you have a package named
widget that has the __init__.py file in it. Remember that, this
__init__.py file is the entry point for packages. We will use it in a
minute.

page Package:
This will be the package for the pages in our project. Each page
will be in a separate folder, in which we will have a class for that
page. The folders for pages are: home , movie_detail , and
movie_list .
To create the page package, in PyCharm right click on the
project name and select New > Python Package. Name the
package as page and hit Enter. Now you have a package named
page that has the __init__.py file in it.
Please see the image below for the packages and folders in
them:

Figure 10-12: Packages and the Window class in


widget/window/Window.py

Now that we have our packages created, let’s define the related
classes. We will start by the definition of the Window class.

Window
The Window class will work as a wrapper for the main
window and related functionalities. It will be located in a folder
named window in the widget package (see Figure 10-12). So you
have to create the window folder first, then the class in it. The file
name for the class Window.py which is the same as the class
name it contains. So the full path for the class is:
widget/window/Window.py . Here is the code for the Window
class:

[11]: 1 # widget/window/Window.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.geometry import GEOMETRY
6
7 class Window:
8 """
9 It creates tkinter main window.
10 """
11 # constructor
12 def __init__(self, title):
13 self.window = tk .Tk()
14 self.window.title(title)
15 self.window.configure(bg=COLORS.BLACK)
16 self.set_size()
17
18 # tkinter start
19 def start_method(self):
20 self.window.mainloop()
21
22 # method for window width & height
23 def set_size(self):
w, h = GEOMETRY .MAIN_WINDOW_WIDTH,
24
GEOMETRY.MAIN_WINDOW_HEIGHT
25 self.window.geometry('%dx%d' % (w, h))
In cell 11, we have the Window class in Window.py file. Here
is the explanation for each line:
In line 4, we import the COLORS class from the data folder
as: from data.colors import COLORS. Please be careful that,
the location of the class is data.colors which means: the colors
file in the data folder.
In line 5, we import the GEOMETRY class from the
data.geometry file as: from data.geometry import
GEOMETRY .
The class constructor, __init__() method in line 12, takes the
one argument as title . And inside the method we create an
attribute called self.window . This is the main window which is an
instance of tk.Tk() class. We create it as: self.window = tk.Tk().
We will use it as the parent for all of our widgets the main.py file.
In line 14, we set the title for self.window as:
self.window.title(title) .
In line 15, we set the background color for the main window as:
self.window.configure(bg=COLORS.BLACK) . The BLACK
color comes from the COLORS class as: COLORS.BLACK .
Since it is a class attribute, we can access it with the class name.
In line 16, we call another method in the class which is
set_size() . We call it as: self.set_size() . This is an instance
method, because its first parameter is the self . That’s why, we
have to call it on the self object.
In line 19, we have the start_method() . This method is
responsible for keeping our application alive because it calls the
mainloop on the main window as: self.window.mainloop() .
Remember that self.window is our main window, and mainloop
is mandatory for a Tkinter app to run properly.
In line 23, we have the definition of the set_size() method. It is
responsible for setting the width and height of the main window. It
first imports the MAIN_WINDOW_WIDTH and
MAIN_WINDOW_HEIGHT constants from the GEOMETRY
class. And names them as w and h with tuple assignment as: w, h
= GEOMETRY.MAIN_WINDOW_WIDTH,
GEOMETRY.MAIN_WINDOW_HEIGHT . Then it passes these
values to the geometry() method of the main window as:
self.window.geometry('%dx%d' % (w, h)). This is how we set
the main screen size in Tkinter.
Now we finished the definition of the Window class, but it’s
not enough to be able to use it. Since it is in a package called
widget , we have to import it in the __init__.py file of this
package. Let’s do it:

[12]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window

In cell 12, you see the code in thee __init__.py file of widget
package. This file is the entry point of this package, so it is
responsible for doing all the imports for the classes and objects that
belong to the package.
In line 9, we import the Window class as: from
.window.Window import Window. Please be careful that the file
path starts with a dot (.) and this is not a mistake. The dot (.)
means the current package. So here is how we read the statement
below:
.window.Window : in the current package we have a folder
called window and in that folder we have a file called Window .
The general syntax for importing classes in the __init__.py
files is: from .[file_path] import Class. We will use this syntax
very frequently in our project. Now we are ready to use the
Window class in any file in the project.
Before defining new classes, let’s see if our app runs at this
state. Since we already have our Window class that has the main
Tkinter window in it, let’s call it in the main.py file:

[13]: 1 # main.py file


2
3 # import window and frames
4 from widget import Window
5
6 if __name__ == '__main__':
7 # Root Window
8 root = Window( 'Movie Library')
9
10 # LEFT FRAME
11
12 # RIGHT FRAME
13
14 # start root window -> mainloop()
15 root.start_method()

In cell 13, you see the main.py as modified. In line 4, we


import the Window class from the widget package as: from
widget import Window. Since we already imported the Window
class in the __init__.py file (see cell 12) of the widget package, it
is very easy to import it here. We just type the package name and
the class name.
In line 8, we instantiate a Window object as root , by calling
the Window class by passing a title. Here is the code: root =
Window('Movie Library'). And this root object has everything
we need in it. It has the main Tkinter window ( self.window ) and
the start_method() . You can revisit cell 11, if you want to be
sure.
In line 15, we call the start_method() as:
root.start_method() . And this is all we need to run our app for the
first time.
Run the main.py file in PyCharm, and you should see the
screen below:
Figure 10-13: Movie Library app running for the first time

If you don’t see the screen in Figure 10-13, then most probably
there are some errors somewhere in your code. You should debug
each file one by one to find and fix the bugs. If you see the screen
the same as here, then you are good to go. We can move on.

Left Frame - Part 1


Let’s start to build our left frame. It is going to be a large class,
so we will build it in three parts. In this first part, our aim to create
the LeftFrame class and add a button to it. We will replace that
button with menu buttons later on, but for the time being, it will be
enough to see if everything works correct. Then we will call the
LeftFrame from the main.py file, to be able to see it on the
screen.
To create the LeftFrame class we will create a folder and
Python file first. In the widget package, create a directory named
left_frame . And in the left_frame folder create a Python file
named LeftFrame.py . This is where we define our LeftFrame
class. Here is the initial code for our left frame:

[14]: 1 # widget/left_frame/LeftFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 class LeftFrame:
7 """
8 Class for Left Frame to hold the menu items.
9 """
10
11 def __init__(self, window, name):
12 self.frame = tk .Frame(
13 master=window,
14 name=name,
15 bg=COLORS.GRAY
16 )
17 self.master = window
18 self.add_frame()
19 self.add_button()
20
21 def add_frame(self):

self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=


22 (62, 0))
23
24 def add_button(self):
btn = tk .Button(master=self.frame, text='Left
25
Menu Button')
26 btn.pack()

In cell 14, you see the initial state of the LeftFrame class. In
its __init__() method, it takes two parameters as window and
name .
In line 12, we create a Tkinter Frame object and name it as
self.frame . Here is the code for it:
self.frame = tk.Frame(master=window, name=name,
bg=COLORS.GRAY)
This self.frame object is going to be the Frame in this class.
Its master is the window parameter, which is going to be the main
Tkinter window. Its name comes from the name parameter and its
background color is COLORS.GRAY . We will change this color
later on.
We also need a master attribute in the current class, that’s why
in line 17, we have self.master = window. This is needed to be
able to store the main window object throughout the class. Its
name is self.master from now on in this class.
In line 18, we call the self.add_frame() method.
In line 19, we call the self.add_button() method.
You see the definition of the add_frame() method in line 21.
This method is responsible for packing the current frame
( self.frame ) in its master. Remember that, to be able to display an
object on Tkinter window we have to pack it. Here is the code for
packing: self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=(62,
0)) . The side parameter is tk.LEFT , which means this frame will
be displayed on the left of the main window. We pass the value of
tk.Y (means Y axis) to the fill parameter, which means we want it
to fill the vertical axis. The last parameter is pady , which is simply
the padding in the Y axis. The value is a tuple of two numbers as
(62, 0). The first number, 62 , is the padding amount (in text units)
from the top and the second one, 0 , is the padding from the
bottom.
In line 24, we have the add_button() method. It creates a
Tkinter Button as: btn = tk.Button(master=self.frame,
text='Left Menu Button'). The master for this btn object is
self.frame , which means this button will be placed in the current
frame (left frame). And then we pack the button as: btn.pack() .
We finished the first form of the LeftFrame , which we will
revisit a lot in the next sections. But for now, let’s see how it looks
on the main window. To display it, we have to call it from the
main.py file. But to be able to call it from main.py file, we have
to import it in the __init__.py file of the widget package. Let’s
do it first:

[15]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame

In cell 15, we import the LeftFrame class in the __init__.py


file of the widget package as: from .left_frame.LeftFrame
import LeftFrame. Now we can call if in the main.py file and
see it on the screen. Let’s do it:

[16]: 1 # main.py file


2
3 # import window and frames
4 from widget import Window, LeftFrame
5
6 if __name__ == '__main__':
7 # Root Window
8 root = Window( 'Movie Library')
9
10 # LEFT FRAME
left_frame = LeftFrame(root .window,
11
'leftFrame')
12
13 # RIGHT FRAME
14
15 # start root window -> mainloop()
16 root.start_method()

In cell 16, we import the LeftFrame class from the widget


package. Then in line 11, we create the left_frame object as:
left_frame = LeftFrame(root.window, 'leftFrame'). As you
see, we pass the root.window for the window parameter and
'leftFrame' for the name .
Now if you run the main.py file, which means running the
project, you should see the screen below, which has the left frame
and a button in it:
Figure 10-14: Initial state of the Left Frame

Left Frame - Part 2


Let’s add the menu buttons to the LeftFrame . Remember that
we have a MENU dictionary in data/menus.py file. We will
import this dictionary and create the menu buttons. Here is the
modified version of the LeftFrame class:

[17]: 1 # widget/left_frame/LeftFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 class LeftFrame:
8 """
9 Class for Left Frame to hold the menu items.
10 """
11
12 def __init__(self, window, name):
13 self.frame = tk .Frame(
14 master=window,
15 name=name,
16 bg=COLORS.GRAY
17 )
18 self.master = window
19 self.add_frame()
20 self.add_menus()
21
22 def add_frame(self):
self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=
23
(62, 0))
24
25 def add_menus(self):
26 # add menus in loop
27 for menu_key, menu_text in MENU .items():
28 self.add_button(menu_text)
29
30 def add_button(self, text):
31 btn = tk .Button(master=self.frame, text=text)
32 btn.pack()

In cell 17, we have redefined the LeftFrame class. We import


the MENU dictionary as: from data.menus import MENU.
In line 25, we define a new method named add_menus . It
reads the keys and values in the MENU dictionary in a for loop
and calls the add_button() method as:
self.add_button(menu_text) . It passes the menu_text as the
button text.
In line 30, we redefine the add_button() method. This time it
takes the text as the parameter and passes this text to the Tkinter
Button class as: btn = tk.Button(master=self.frame,
text=text) . Then it packs the btn object.
You should see the menu buttons on the left when you run the
main.py file now. See the image below:
Figure 10-15: Menu buttons on the left frame

Now let’s make our menu buttons prettier. We want them to be


full width as their parent container, which is the LeftFrame . And
also we want the About button to be at the bottom left corner.
Let’s do it now.
Instead of doing this modifications in the LeftFrame class, we
will create a separate Button class. We want this class to be
responsible for button formatting and taking necessary actions
when clicked.
Button Class:
Under the widget package create a new folder called button .
In this folder create a new Python file as Button.py . We will
define our Button class in this file. Be careful that, the file name
and the class name are the same, as we did before. This class will
be a wrapper class for the Tkinter Button . Why do we need a
wrapper class? Because, we can add new functionalities to existing
ones that comes from Tkinter and that will become our own
implementation in our project. The wrapper will give the complete
control over the Button objects that will be created in the project.
Here is the code in the Button class:

[18]: 1 # widget/button/Button.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 # Wrapper Class -> wrapped the tkinter.Button()
7 class Button:
8 """It will create Tkinter Button."""
9 def __init__(self, master, name, text,
10 fg, bg, width, height,
11 handle_click,
12 padx=0, pady=0, side=tk.TOP):
13 self.button = tk .Button(
14 master=master,
15 name=name,
16 text=text,
17 fg=fg,
18 bg=bg,
19 width=width,
20 height=height,
21 activebackground=COLORS.ORANGE)
22 self.padx = padx
23 self.pady = pady
24 self.side = side
25 self.add_button()
26
27 def add_button(self):
28 self.button.configure(font=('Arial', 12))
29 self.button.pack(
30 padx = self.padx,
31 pady = self.pady,
32 side = self.side)

In cell 18, you see the definition of our Button class. In the
__i n i t __() method we have parameters as: master , name , text ,
fg , bg , width , height , handle_click , padx=0 , pady=0 ,
side=tk.TOP . Most of them are self-explanatory so no need to
repeat them. But handle_click is new. We will use it as the event
handler when the user clicks on our Button . Since we want this
Button class to be a generic class, we cannot define a static click
handler. That’s why it is parametric here.
In line 13, in the __i n i t __() method, we create a button
attribute as self.button . This is the instance of the Tkinter Button
and we will be using it in our class as the button object. We pass
the parameters we get from the caller. What’s new here is
activebackground parameter. It represents the background color
when a button is active, which means when it is clicked.
In line 25, we call the self.add_button() method to configure
and pack the button object, which is self.button .
We finished the definition of our Button class. Let’s add it to
the __init__.py file of the widget package:
[19]: 1 # __init__.py file of the widget package
2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame
11 from .button.Button import Button

In cell 19, we import the Button class in the __init__.py file


of the widget package as: from .button.Button import Button.
Now we can use this class in the LeftFrame. We will also modify
LeftFrame a bit to call our own Button class instead of Tkinter
Button. Let’s do it and see the result:

[20]: 1 # widget/left_frame/LeftFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 # circular import error
8 # from widget import Button
9
10 # exact import
11 from widget.button.Button import Button
12
13 class LeftFrame:
14 """
15 Class for Left Frame to hold the menu items.
16 """
17
18 def __init__(self, window, name):
19 self.frame = tk .Frame(
20 master=window,
21 name=name,
22 bg=COLORS.GRAY
23 )
24 self.master = window
25 self.add_frame()
26 self.add_menus()
27
28 def add_frame(self):
29 self.frame.pack(side=tk.LEFT, fill=tk.Y,
30 pady=(62, 0))
31
32 def add_menus(self):
33 # add menus in loop
34 for menu_key, menu_text in MENU .items():
35 button = Button( self.frame, menu_key,
36 menu_text,
37 COLORS.ORANGE,
38 COLORS.BLACK,
39 18, 2,
40 handle_click=None)

In cell 20, we redefine our LeftFrame class. First we import


our custom Button class.

Wrong Import:
Please examine carefully, the way we import the Button class.
We didn’t import it as: from widget import Button. See the line
8, which is commented out. This will raise a circular import
error. Because this line will import the widget package as a
whole, which contains this LeftFrame class too. That means, we
are trying to import ourselves. So it will not work.

Correct Import:
The correct way to import a class in the same package is to use
the exact import, which means typing the complete file location.
Here is the code: from widget.button.Button import Button.
We write the file location where we want to import the Button
class. And it works.

Inside the LeftFrame class, we deleted the add_button()


method. We don’t need it anymore, because we have our own
Button class. So in the add_menus() method in line 32, inside the
for loop, we call our Button class and pass the necessary
parameters. Since it packs itself in its class, we don’t need it to
pack it here. We pass None for the handle_click parameter for
the time being. We will handle that one later.
You should see a set of good looking buttons when you run the
project now. Just like the image below. And when you click any of
the buttons, its color will change to orange.
Figure 10-15: Formatted Menu buttons with the Button class

Left Frame - Part 3


Our menu buttons look great but they don’t have any
functionalities yet. They don’t take any action when you click on
them. We will fix this in this sub-section. The background for the
left frame is not correct, it should be black as you may remember
from the mockup images. Also the About button should be placed
at the bottom left corner of the screen. Let’s redefine our
LeftFrame class and fix these bugs.
[21]: 1 # widget/left_frame/LeftFrame.py file
2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 # circular import error
8 # from widget import Button
9
10 # exact import
11 from widget.button.Button import Button
12
13 class LeftFrame:
14 """
15 Class for Left Frame to hold the menu items.
16 """
17
18 def __init__(self, window, name):
19 self.frame = tk .Frame(
20 master=window,
21 name=name,
22 bg=COLORS.BLACK
23 )
24 self.master = window
25 self.add_frame()
26 self.add_menus()
27
28 def add_frame(self):
29 self.frame.pack(side=tk.LEFT, fill=tk.Y,
30 pady=(62, 0))
31
# method for click event of menu buttons ->
32
event handler
33 def handle_click(self, event):
34 self.manage_button_colors(event)
35
36 def add_menus(self):
37 # add menus in loop
38 for menu_key, menu_text in MENU .items():
39 if menu_key == 'about':
button = Button( self.frame, menu_key,
40
menu_text,
COLORS.ORANGE,
41
COLORS.BLACK,
42 18, 2,
43 handle_click=self.handle_click,
44 side=tk.BOTTOM)
45 else:
button = Button( self.frame, menu_key,
46
menu_text,
COLORS.ORANGE,
47
COLORS.BLACK,
48 18, 2,
49 handle_click=self.handle_click,
50 side=tk.TOP)
51
52 def manage_button_colors(self, event):
53 # clicked button -> event.widget
# all the menu buttons ->
54
event.widget.master.children
for child in
55
event.widget.master.winfo_children():
56 if child == event .widget:
child.configure(bg=COLORS.ORANGE,
57
fg=COLORS.WHITE)

58 else:
child.configure(bg=COLORS.BLACK,
59
fg=COLORS.ORANGE)

In cell 21, we have the modified version of our LeftFrame


class. We first change the background color to be
COLORS.BLACK in the __init__() method.
In line 33, we define a new method called handle_click . This
is where we will handle the click events for our menu buttons. It
takes the event as the parameter. You will see how we pass this
event in a minute. This method calls another method, which is
manage_button_colors .
In line 36, we redefine the add_menus() method. This time we
check the value of the menu_key . If its value is ‘about’ then we
put this Button in the bottom by passing side=tk.BOTTOM . If
not then we pass as side=tk.TOP . For the handle_click
parameter of the Button class we pass the self.handle_click
method. This is the method we define in line 33. But this is not
enough to be able to handle the click events of the menu buttons.
We have to modify the Button class to bind this method to the
click event. We will do it in a minute.
In line 52, we have definition of the manage_button_colors()
method. It takes the event parameter from the handle_click
method. Its purpose is to modify the button background and
foreground colors. The idea is simple; for the clicked button it will
set background ( bg ) color to orange and the foreground ( fg ) color
to white. For other buttons, it will set bg to black and fg to
orange. The clicked button is the button that sends the event. So
event.widget gives us the clicked button object. For other buttons
we need to get the master of the clicked button. This is the parent
of all the menu buttons which is the LeftFrame itself. We find it
as event.widget.master . And to get all of its children we need to
call a special method as winfo_children() . This method gives the
child widgets of the parent one. In our case the child widgets means
all of the buttons in the LeftFrame .
To make it easy to understand here are the objects:
event.widget is the clicked button (the button who sends the
event)
event.widget.master is the master (parent) of the clicked
button (the LeftFrame itself)
event.widget.master.winfo_children() is the list of all the
children in the master (menu buttons in the LeftFrame )
So event.widget.master.winfo_children() gives us all the
buttons in the LeftFrame including the clicked one. So we will
loop over all the buttons and check if anyone is the clicked one
( event.widget ). This what we do in line 56 as: child ==
event.widget . If that’s True then we configure the button as
follows: child.configure(bg=COLORS.ORANGE,
fg=COLORS.WHITE) . If not then the configuration is:
child.configure(bg=COLORS.BLACK,
fg=COLORS.ORANGE) .
We have finished the modifications in the LeftFrame class.
But as I stated earlier this is not enough to catch the click events of
the buttons. In the Button class we need to bind the left mouse
click to the handle_click parameter, which is the event handler
we send to the buttons. Let’s do that:

[22]: 1 # widget/button/Button.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 # Wrapper Class -> wrapped the tkinter.Button()
7 class Button:
8 """It will create Tkinter Button."""
9 def __init__(self, master, name, text,
10 fg, bg, width, height,
11 handle_click,
12 padx=0, pady=0, side=tk.TOP):
13 self.button = tk .Button(
14 master=master,
15 name=name,
16 text=text,
17 fg=fg,
18 bg=bg,
19 width=width,
20 height=height,
21 activebackground=COLORS.ORANGE)
22 self.padx = padx
23 self.pady = pady
24 self.side = side
25 self.add_button()
26 # bind the event handler
27 self.bind_event(handle_click)
28
29 def add_button(self):
30 self.button.configure(font=('Arial', 12))
31 self.button.pack(
32 padx = self.padx,
33 pady = self.pady,
34 side = self.side)
35
36 # event binding to button
37 def bind_event(self, handle_click):
# in tkinter -> .bind() -> left mouse click ->
38
<Button-1>
39 self.button.bind('<Button-1>', handle_click)
In cell 22, we modify the Button class to bind the click event
handler. In the __init__() method we call a method as
self.bind_event(handle_click) and as you see we pass the
handle_click parameter. Actually this handle_click is a function
(method) that we pass while creating Button objects. In Python,
functions are first-class citizens, which means you can pass them
as parameters like any other data structure. And that’s what we do
here.
In line 37, we have the definition of the bind_event() method
which takes the handle_click as its parameter. In Tkinter, the
name for the left mouse click is '<Button-1>' . And we bind it to
the self.button as: self.button.bind('<Button-1>',
handle_click) . Remember that self.button is actually a Tkinter
Button, and it has the bind() method by default. This method takes
two parameters, the event name and the function that will be used
as the event handler. So when a left mouse click event is triggered
on our menu buttons, the handle_click function will be called.
That’s the meaning of event binding.
Now we are all set about events and LeftFrame buttons. Let’s
run the main.py file and see the result:
Figure 10-17: Left Menu buttons formatted and click event is working

If you can see the screen in Figure 10-17, when you run the
project, then you are ready for the next step which is the Right
Frame configuration. If you don’t see this screen or getting any
errors then it’s time for debugging for you.

Right Frame
Now it’s time to create the right frame. We will define a
RightFrame class for this purpose. It’s going to keep the pages on
the right hand side of the screen. Actually these pages will be the
content of our application. Remember that we had the MENU
dictionary (see cell 7) for the pages. Here are our pages: Home,
Movie List, Movie Detail, and About. We will create the first three
in this project and I will leave the About page to you. And the
RightFrame class will be the container for these pages.
To create the RightFrame class we will create a folder and
Python file first. In the widget package, create a directory named
right_frame . And in the right_frame folder create a Python file
named RightFrame.py . This is where we define our RightFrame
class. Here is the initial code for our right frame:

[23]: 1 # widget/right_frame/RightFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 class RightFrame:
7 """It will hold the pages on the right frame."""
8
9 # class attribute
10 bg_color = COLORS .ORANGE
11
def __init__(self, window, name,
12
relief=tk.SUNKEN, side=tk.LEFT):
13 self.frame = tk .Frame(
14 master=window,
15 name=name,
16 relief=relief,
17 bg=RightFrame.bg_color
18 )
19 self.side = side

20 self.add_frame()
21
22 def add_frame(self):
23 # frame content
24 self.frame_content()
self.frame.pack(side=self.side, fill=tk.BOTH,
25
expand=True)
26
27 def frame_content(self, page_name='home'):
28 if page_name == 'home':
29 # add home page
30 pass
31 elif page_name == 'movieList':
32 # add movie list page
33 pass
34 elif page_name == 'movieDetail':
35 # add movie detail page
36 pass

In cell 23, we have the definition of our RightFrame class. In


the __init__() method it creates a Tkinter Frame as self.frame .
As the background ( bg ) color it uses a class attribute as
RightFrame.bg_color . Remember that class attributes are
accessible with the class name. We have a new parameter here
which is relief . Relief is used for border decoration. The possible
values are: SUNKEN , RAISED , GROOVE , RIDGE , and
FLAT . We selected SUNKEN here.
In line 19, in the __init__() method, we call the
self.add_frame() method.
In line 21, you see the add_frame() method. It call another
method as self.frame_content() . Then it packs the self.frame as:
self.frame.pack(side=self.side, fill=tk.BOTH, expand=True).
Here, fill=tk.BOTH means, we want it to fill both directions,
horizontal and vertical. In other words we want this frame to fill
both X and Y axes. The expand=True parameter means, we
want this frame to expand if the main screen size changes. So it
will be adaptive.
In line 26, we have the frame_content() method. This is
where we will call our page classes. For the time being, we don’t
have these classes yet, but we will define them very soon. We get
the page_name as the parameter, which is ‘home’ by default.
And according to the page name, we will call the related page
class. Remember these page names the keys of the MENU
dictionary we defined. These keys are: 'home' , 'movieList' and
'movieDetail' . (We will not create a page for the ‘about’ key.)
To be able to display the RightFrame we have to import it in
the __init__.py file of the widget package. Then we have to call
in in the main.py file. Let’s complete these steps:

[24]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame
11 from .button.Button import Button
from .right_frame.RightFrame import
12
RightFrame

In cell 24, in the __init__.py file of the widget package, we


import the RightFrame class as: from
.right_frame.RightFrame import RightFrame.
Now let’s call this class in the main.py file:

[25]: 1 # main.py file


2
3 # import window and frames
from widget import Window, LeftFrame,
4
RightFrame
5
6 if __name__ == '__main__':
7 # Root Window
8 root = Window( 'Movie Library')
9
10 # LEFT FRAME
left_frame = LeftFrame(root .window,
11
'leftFrame')
12
13 # RIGHT FRAME
right_frame = RightFrame(root .window,
14
'rightFrame')
15
16 # start root window -> mainloop()
17 root.start_method()

In line 25, in the main.py file we call the RightFrame as:


right_frame = RightFrame(root.window, 'rightFrame'). And
we pass root.window as the master and 'rightFrame' as the
name.
If we run our application at this step, we will see the right frame
in orange on the right hand side. Here it is:
Figure 10-18: Right frame on the screen

Home Page
Let’s create the home page of our application. We will create
the all of the pages under the page package. For the home page,
the class name will be in the same name as Home . Let’s start by
creating a new folder named home in the page package. And in
this home directory create a new Python file as Home.py .
Remember by convention, in our project the file names and the
classes are usually the same. Here is the initial content of the
Home class:
[26]: 1 # page/home/Home.py file
2
3 import tkinter as tk
4
5 from data.colors import COLORS
6
7 class Home:
8 """It creates the Home Page."""
9
10 def __init__(self, window, bg_color,
11 relief=tk.SUNKEN, side=tk.LEFT):
12 self.frame = tk .Frame(
13 master=window,
14 name='home',
15 relief=relief,
16 bg=bg_color)
17 self.side = side
18 self.bg_color = bg_color
19 self.frame_content()
20 self.add_frame()
21
22 def add_frame(self):
self.frame.pack(side=self.side, fill=tk.BOTH,
23
expand=True)
24
25 def frame_content(self):
26 course = tk .Label(self.frame,
27 text='Python Hands-On',
28 font=('Helvetica', 18, 'bold'),
29 fg=COLORS.BLACK,
30 bg=self.bg_color)
31 course.place(x=315, y=50)
In cell 26, we have our Home class. In its __init__() method,
it creates a frame as self.frame by calling tk.Frame. The important
parameters to the init method are window which will be the master
and bg_color for background color. We have to more parameters
as relief and side which have default values. In the init method
we call two methods which are self.frame_content() and
self.add_frame() .
In line 22, we have the add_frame() method which simply
packs the self.frame . Here is the code:
self.frame.pack(side=self.side, fill=tk.BOTH, expand=True).
In line 25, you see the frame_content() method. It creates a
Tkinter Label and names it as course . It sets the text , font , fg
and bg parameters of the label. This will hold the course name on
our main page. Then in line 31, it puts this label on an exact
location on the screen by providing the x and y coordinates. Here
it is: course.place(x=315, y=50).
Now we should import the Home class in the __init__.py file
of the page package. Below is the code for that:

[27]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail
6 """
7
8 from .home.Home import Home

As the last step, to be able to display the Home page, we have


to call it in the RightFrame . Remember that, the RightFrame is
where we keep our pages.
[28]: 1 # widget/right_frame/RightFrame.py file
2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 from page import Home
7
8 class RightFrame:
9 """It will hold the pages on the right frame."""
10
11 # class attribute
12 bg_color = COLORS .ORANGE
13
def __init__(self, window, name,
14
relief=tk.SUNKEN, side=tk.LEFT):
15 self.frame = tk .Frame(
16 master=window,
17 name=name,
18 relief=relief,
19 bg=RightFrame.bg_color
20 )
21 self.side = side
22 self.add_frame()
23
24 def add_frame(self):
25 # frame content
26 self.frame_content()
self.frame.pack(side=self.side, fill=tk.BOTH,
27
expand=True)
28
29 def frame_content(self, page_name='home'):
30 if page_name == 'home':
31 # add home page
32 Home(self.frame, RightFrame.bg_color)
33 elif page_name == 'movieList':
34 # add movie list page
35 pass
36 elif page_name == 'movieDetail':
37 # add movie detail page
38 pass

In cell 28, you see the current state of the RightFrame . It is


almost the same as the previous definition in cell 23. The only
thing that we add here is the Home class. In line 6, we import the
Home class from the page package as: from page import Home.
And in line 32, we call the Home class because that’s where we
have the condition for page_name == 'home'. Here is how we
call it: Home(self.frame, RightFrame.bg_color).
And if you run the application now, you should see the image
below, in which we have a text on the right frame:
Figure 10-19: Home page on the right frame

Now that we have a working Home page, let’s continue to add


more content to it. Remember in our mockup we had the instructor
name, Python logo and the project name.

Pillow Package:
For rendering the Python logo, which is an image, we need to
install a third-party package that is the Pillow package. Pillow is a
not built-in package in Python. So we have to install it in the virtual
environment of our current project. We will install it in PyCharm.
If you need help on how to install packages in PyCharm please
revisit the Installing Packages section in Chapter 4 – Modules and
Packages. Where you will see the necessary steps in detail. Below
you can see an image for the package you should install. Just click
on Install Package after you find it.

Figure 10-20: Installing the Pillow package in PyCharm

Now that we install the necessary package let’s modify the


Home class:

[29]: 1 # page/home/Home.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from PIL import Image, ImageTk
6
7 class Home:
8 """It creates the Home Page."""
9
def __init__(self, window, bg_color,
10
relief=tk.SUNKEN, side=tk.LEFT):
11 self.frame = tk .Frame(
12 master=window,
13 name='home',
14 relief=relief,
15 bg=bg_color
16 )
17 self.side = side
18 self.bg_color = bg_color
19 self.frame_content()
20 self.add_frame()
21
22 def add_frame(self):
self.frame.pack(side=self.side, fill=tk.BOTH,
23
expand=True)
24
25 def frame_content(self):
26 course = tk .Label(self.frame,
27 text='Python Hands-On',
28 font=('Helvetica', 18, 'bold'),
29 fg=COLORS.BLACK,
30 bg=self.bg_color)
31 course.place(x=315, y=50)
32

33 instructor = tk .Label(self.frame,
34 text='(Musa Arda)',
35 font=('Helvetica', 18, 'bold'),
36 fg=COLORS.BLACK,
37 bg=self.bg_color)
38 instructor.place(x=344, y=105)
39
40 self.render_image()
41
42 project = tk .Label(self.frame,
text='Capstone Project - Movie
43
Library with Tkinter',
44 font=('Helvetica', 18, 'bold'),
45 fg=COLORS.WHITE,
46 bg=COLORS.BLACK)
47 project.place(x=140, y=765)
48
49 def render_image(self):
load =
50
Image.open('images/home/python_logo.png')
51 render = ImageTk .PhotoImage(load)
img_lb = tk .Label(self.frame, image=render,
52
bg=self.bg_color)
53 img_lb.image = render
54 img_lb.place(x=136, y=180)

In cell 29, you see the final form of our Home class. We import
two classes from the PIL package. PIL is a sub package inside
the Pillow package which we installed. Here is the import
statement: from PIL import Image, ImageTk.
We add new widgets in the frame_content() method.
In line 33, we add an instructor field, which stores the
instructor name and place it on the screen.
In line 40, we call the render_image() method to render the
python_logo.png image.
In line 42, we add the last field as project by passing the
project name to the tk.Label() widget.
In line 49, you see the definition of the render_image()
method. To render an image in a Tkinter application, first we need
to open the image. We use the Image class from PIL to do this in
line 50. Here is the code: load =
Image.open('images/home/python_logo.png') . We call the
opened image as load .
In line 51, we call ImageTk.PhotoImage class and pass the
load object as: render = ImageTk.PhotoImage(load). And we
name the object we instantiate as render . This will be our image
to pass to the tk.Label .
In line 52, we call tk.Label class to render a label on the
screen. Here is the code : img_lb = tk.Label(self.frame,
image=render, bg=self.bg_color).
This img_lb label will display the image. To make it work we
have to complete two steps:
First, we need to pass our render object to the image
parameter of the tk.Label class as: image=render
Second, we need to explicitly set the image attribute of our
img_lb as in line 53: img_lb.image = render
And finally we place the img_lb on the screen with setting the
x and y coordinates.
Now let’s run the project and see our Home page as displayed
in the image below:
Figure 10-21: Home page finalized

If you see the same output as in Figure 10-21, then it’s great.
Your project works as expected. If you do not see the Python logo
as a transparent image, which might occur in some cases, don’t
worry. Most probably, the reason is zipping and unzipping of the
project. So, just open the python_logo.png file in an image
processor (like Photoshop) and save it as a transparent png image.
Or you can find a transparent Python logo on the internet and use
it. Just make sure you handle the x and y values for placement if
you change any of the content in home page. Because we used
absolute units while placing our widgets.

Movie List - Part 1


In this sub-section we will create the MovieList class which
will be our movie list page. This is the second page in our
application and has a table like structure in it. The data for the
movies comes from the imdb_top_250.csv file which is in the
data folder. This data was acquired by doing web scraping with
Python on the IMDB Top 250 Movies list page.
Let’s start by creating a new folder named movie_list in the
page package. And in this movie_list directory create a new
Python file as MovieList.py . The file name and the class name are
again the same. It’s going to be a very large class so we will define
it in multiple sub-sections. Here is the initial content of the
MovieList class:

[30]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4
5 from data.colors import COLORS
6
7 class MovieList:
8 """This class is the frame for movie list."""
9
10 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
11
'imdbRating', 'imdbVotes']
12
def __init__(self, master, bg_color,
13 relief=tk.SUNKEN, side=tk.LEFT):
14 self.frame = tk .Frame(
15 master=master,
16 name='movieList',
17 relief=relief,
18 bg=bg_color
19 )
20 self.side = side
21 self.movies = []
22 self.add_frame()
23
24 def add_frame(self):
25 self.add_page_title('Movie List')
self.frame.pack(side=self.side, fill=tk.BOTH,
26
expand=True)
27
28 def add_page_title(self, title):
29 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
30
fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
31
padx=1, pady=(0, 8), sticky='we')

In cell 30, we have our MovieList class. It defines a class


attribute named columns to keep the column names for our list.
In the __init__() method, it creates a tk.Frame object as
self.frame and passes the necessary parameters. To store the
movies data, it creates an empty list as self.movies . And finally it
calls the add_frame() method.
In the add_frame() method in line 24, it first sets the page title
by calling another method which is add_page_title . Then it packs
the current frame on the screen by filling both x and y axes as:
self.frame.pack(side=self.side, fill=tk.BOTH, expand=True).
In line 28, we see the add_page_title method. It creates a
tk.Label object with necessary parameters and names it as lbl .
Then in line 31, it puts this lbl object on the screen. This time we
use the grid() method for placement. The grid() method takes
row and column numbers as parameters. We also set the
columnspan , padx , pady and sticky parameter values. The
sticky parameter value is ‘we’ which means ‘west-east’ that
allows the title to stick in the horizontal direction.
As the last step, let’s add our MovieList to the __init__.py
file of the page package:

[31]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail
6 """
7
8 from .home.Home import Home
9 from .movie_list.MovieList import MovieList

To be able to display our MovieList on the screen, we have to


do two things:
First, we need to call it from the RightFrame class
Second, we need to change the page when we click on the
menu buttons on the LeftFrame . Remember that, on the left
side of the screen, we have buttons as Home , Movie List,
Movie Detail and About . So when the user clicks on the
Movie List button, we need to render the MovieList class on
the right hand side.
Let’s add these functionalities in the next sub-section.

Movie List - Part 2


To display our MovieList class on the right panel of the
screen, we will start by modifying the LeftFrame class. Why?
Because the menu buttons are in the LeftFrame . And we will
modify the click event handler ( handle_click ) which we defined
earlier. So the handle_click() method will be capable of switching
the pages according to the button which is clicked. We also have to
modify the RightFrame too.
Here are the steps for switching the pages:
First, in the LeftFrame , we will get the menu button which is
clicked
Second we will clear (destroy) the old page which is present in
the RightFrame . In other words, we will remove the old page
by destroying the children of the RightFrame .
Third, in the LeftFrame , we will call the new page on the
RightFrame that will be displayed according to the clicked
button.
Let’s start with the LeftFrame :

[32]: 1 # widget/left_frame/LeftFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 # circular import error
8 # from widget import Button
9
10 # exact import
11 from widget.button.Button import Button
12 from widget.right_frame.RightFrame import
RightFrame
13
14 class LeftFrame:
15 """
16 Class for Left Frame to hold the menu items.
17 """
18
19 def __init__(self, window, name):
20 self.frame = tk .Frame(
21 master=window,
22 name=name,
23 bg=COLORS.BLACK
24 )
25 self.master = window
26 self.add_frame()
27 self.add_menus()
28
29 def add_frame(self):
30 self.frame.pack(side=tk.LEFT, fill=tk.Y,
31 pady=(62, 0))
32
# method for click event of menu buttons ->
33
event handler
34 def handle_click(self, event):
35 self.manage_button_colors(event)
36 page_name = str(event.widget).split('.')[2]
37
38 # self is left frame -> master = root
39 # right frame is in children of master
40 rightFrame = self.master.children['rightFrame']
41
42 # destroy children
43 RightFrame.destroy_children(rightFrame)

44
45 # add new page -> page_name
RightFrame.frame_content(rightFrame,
46
page_name)
47
48 def add_menus(self):
49 # add menus in loop
50 for menu_key, menu_text in MENU .items():
51 if menu_key == 'about':
button = Button( self.frame, menu_key,
52
menu_text,
COLORS.ORANGE,
53
COLORS.BLACK,
54 18, 2,
55 handle_click=self.handle_click,
56 side=tk.BOTTOM)
57 else:
button = Button( self.frame, menu_key,
58
menu_text,
COLORS.ORANGE,
59
COLORS.BLACK,
60 18, 2,
61 handle_click=self.handle_click,
62 side=tk.TOP)
63
64 def manage_button_colors(self, event):
65 # clicked button -> event.widget
# all the menu buttons ->
66
event.widget.master.children
for child in
67
event.widget.master.winfo_children():
68 if child == event .widget:
child.configure(bg=COLORS.ORANGE,
69 fg=COLORS.WHITE)
70 else:
child.configure(bg=COLORS.BLACK,
71
fg=COLORS.ORANGE)

In cell 32, we have the new version of the LeftFrame class.


We modify the handle_click() method.
In this method in line 36, we get the clicked page name from
the event.widget as: page_name = str(event.widget).split('.')
[2] . This statement gives us the page name like ‘home’ ,
‘movieList’ , ‘movieDetail’ or ‘about’ . Remember that, these
are the names which we set in the init methods of the Home or
MovieList classes.
In line 40, we get the rightFrame object as: rightFrame =
self.master.children['rightFrame'] . Here self.master is the
main window and in its children dictionary we get the
'rightFrame' .
In line 43, we call a static method from the RightFrame class
as: RightFrame.destroy_children(rightFrame) . The method
name is destroy_children and we will define it in a minute. This
method is needed to destroy all the children, which are pages, of
the RightFrame .
Inline 46, we call the frame_content() method of the
RightFrame class as: RightFrame.frame_content(rightFrame,
page_name) . We actually have this method already, but it needs
some modifications. This method will create the new page for us.
Which is the page whose menu button is clicked.
That’s all about the LeftFrame modifications, now let’s move
on with the RightFrame :

[33]: 1 # widget/right_frame/RightFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 from page import Home, MovieList
7
8 class RightFrame:
9 """It will hold the pages on the right frame."""
10
11 # class attribute
12 bg_color = COLORS .ORANGE
13
def __init__(self, window, name,
14
relief=tk.SUNKEN, side=tk.LEFT):
15 self.frame = tk .Frame(
16 master=window,
17 name=name,
18 relief=relief,
19 bg=RightFrame.bg_color
20 )
21 self.side = side
22 self.add_frame()
23
24 def add_frame(self):
25 # frame content
26 self.frame_content()
self.frame.pack(side=self.side, fill=tk.BOTH,
27
expand=True)
28
29 def frame_content(self, page_name='home'):
30 try:
31 incoming_frame = self.frame
32 except:
33 incoming_frame = self
34 finally:
35 if page_name == 'home':
36 # add home page
Home(incoming_frame,
37
RightFrame.bg_color)
38 elif page_name == 'movieList':
39 # add movie list page
MovieList(incoming_frame,
40
RightFrame.bg_color)
41 elif page_name == 'movieDetail':
42 # add movie detail page
43 pass
44
# static method -> Class Method (no self
45
parameter)
46 def destroy_children(frame):
47 # destroy the children
48 for child in frame .winfo_children():
49 child.destroy()

In cell 33, we have the modified version of the RightFrame.


We changed the code in the frame_content() method, because
we want to call it from both classes. Here are two ways that we call
this method:
1. From the LeftFrame : As we already saw the line 46 in cell
32 which was: RightFrame.frame_content(rightFrame,
page_name) . In this line we call frame_content() method by
passing the rightFrame object. And this object has no
attribute as frame . Because it is the frame itself. (This will
make sense in a minute.)
2. From the RightFrame itself: We call it with the self
parameter when it is called inside the RightFrame class as:
self.frame_content() . And the self has an attribute called
self.frame . Because the method is called in its own class.
To sum up, when we call the frame_content() method from
two different classes, we have to make sure that self.frame is a
valid object. Otherwise we will get an exception. To handle this
exception, we put a try-except-finally structure in the method.
In line 31, inside the try block, we check if the self parameter
has an attribute called frame . Here is the code : incoming_frame
= self.frame. And we name the resulting object as
incoming_frame . If this code runs successfully, then the finally
block will execute and it will call the necessary page.
If the try block generates an error, which means there is no
attribute as self.frame , then the except block will execute. And it
is simply set the self to the incoming_frame . Again the finally
block will execute and it will call the necessary page.
Now we can call the frame_content() method from both the
LeftFrame and the RightFrame itself.
In line 46 we define a new method called destroy_children .
This method takes a frame as the argument and loops over its
children as: frame.winfo_children() . And it destroys each child
one by one as: child.destroy() . Remember that we called this
method from the LeftFrame in cell 32 line 43 as:
RightFrame.destroy_children(rightFrame) .
One more thing about the destroy_children() method is, it is a
static method. Static methods are the methods that do not have a
self parameter. So you have to call it with the class name. That’s
what we do when we called it from the LeftFrame .
If you run the project now, you should see that the button click
works for the Home and Movie List pages. And in the Movie List
page, you see the title. See the image below:
Figure 10-22: Movie List page displayed with menu button

Movie List - Part 3


In this sub-section, we will modify the title of the Movie List
page and create the header row for the movie list table that we will
create. The header row is going to include the column headers for
the list.
Let’s see the new version of our MovieList class:

[34]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 import csv
5
6 from data.colors import COLORS
7
8 class MovieList:
9 """This class is the frame for movie list."""
10
11 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
12
'imdbRating', 'imdbVotes']
13
def __init__(self, master, bg_color,
14
relief=tk.SUNKEN, side=tk.LEFT):
15 self.frame = tk .Frame(
16 master=master,
17 name='movieList',
18 relief=relief,
19 bg=bg_color
20 )
21 self.side = side
22 self.movies = []
23 self.add_frame()
24
25 def add_frame(self):
26 self.add_page_title('Movie List')
27 self.read_csv()
28 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
29
expand=True)
30
31 def add_page_title(self, title):
32 lbl = tk .Label(self.frame, text=title, height=3,
33 bg=COLORS.BLACK,
fg=COLORS.WHITE,
34 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
35
padx=1,
36 pady=(0, 8), sticky='we')
37
38 def read_csv(self):
39 movie_path = 'data/imdb_top_250.csv'
40 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
41
delimiter=';')
42 for movie in movie_dict:
43 self.movies.append(movie)
44
45 def create_page(self):
46 self.add_header_row()
47
48 def add_header_row(self):
for j, column in
49
enumerate(MovieList.columns):
50 if column != 'imdbID':
lbl = tk .Label(self.frame,
51
text=str(column),
width=54, height=2,
52
bg=COLORS.BLACK,
53 fg=COLORS.WHITE,
54 font=('Arial', 10, 'bold'))
55 # configure width
56 if column == 'Id':
57 lbl.configure(text='#', width=4)

58 elif column == 'Year':


59 lbl.configure(width=8)
60 elif column == 'imdbRating':
61 lbl.configure(text='Rating', width=8)
62 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
63
width=12)
64
65 # place in a grid
66 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
67
padx=(0, 10))
68 else:
lbl.grid(row=1, column=j, sticky='we',
69
padx=(0, 1))

In cell 34, you see the modified version of the MovieList class.
In the add_frame() method in line 25, we call two new
methods which are self.read_csv() and self.create_page() .
In the read_csv() method in line 38, we define a movie path
for the csv data that we will read. Then in the with context
manager we open this file path in read mode. And we name the
returning object as file . In line 41, we use csv.DictReader class
on this file object and get the movie dictionary as: movie_dict =
csv.DictReader(file, delimiter=';'). In line 43, inside the for
loop, we append each movie to the self.movies list.
In the create_page() method in line 45, we call another
method as self.add_header_row() . This new method is
responsible for creating the header row.
In the add_header_row() method in line 48, we loop over the
MovieList.columns list, which stores the column headers.
In line 50, inside the if block, we check the column headers
one by one to set the text , width and other necessary parameters.
In line 66, we place the lbl object, which is tk.Label for the
column header, by calling the grid() method as: lbl.grid(row=1,
column=j, sticky='we', padx=(0, 10)). Here j is the column
index. The value for the row parameter is 1 because we have the
page title on row 0 . Finally, we give an extra 10 text units padding,
padx=(0, 10), on the right of the last column which is
'imdbVotes' .
Now you should see the image below when you run the project:

Figure 10-23: The page title and the column headers in the Movie List

Movie List - Part 4


In this sub-section we will create the actual movie list. So far in
our MovieList page we have the page title and the header row as
you see in Figure 10-23. No it’s time to use our movies data and
build a beautiful list of IMDB Top 250 movies.
We will modify our MovieList class and add new methods.
Let’s see the code first:

[35]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 import csv
5 from PIL import Image, ImageTk
6 from data.colors import COLORS
7
8 class MovieList:
9 """This class is the frame for movie list."""
10
11 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
12
'imdbRating', 'imdbVotes']
13
def __init__(self, master, bg_color,
14
relief=tk.SUNKEN, side=tk.LEFT):
15 self.frame = tk .Frame(
16 master=master,
17 name='movieList',
18 relief=relief,
19 bg=bg_color
20 )
21 self.side = side
22 self.movies = []
23 self.add_frame()
24
25 def add_frame(self):
26 self.add_page_title('Movie List')
27 self.read_csv()
28 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
29
expand=True)
30
31 def add_page_title(self, title):
32 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
33
fg=COLORS.WHITE,
34 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
35
padx=1,
36 pady=(0, 8), sticky='we')
37
38 def read_csv(self):
39 movie_path = 'data/imdb_top_250.csv'
40 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
41
delimiter=';')
42 for movie in movie_dict:
43 self.movies.append(movie)
44
45 def create_page(self):
46 self.add_header_row()
47 self.create_table()
48
49 def add_header_row(self):
for j, column in
50
enumerate(MovieList.columns):
51 if column != 'imdbID':
lbl = tk .Label(self.frame,
52 text=str(column),
width=54, height=2,
53
bg=COLORS.BLACK,
54 fg=COLORS.WHITE,
55 font=('Arial', 10, 'bold'))
56 # configure width
57 if column == 'Id':

58 lbl.configure(text='#', width=4)
59 elif column == 'Year':
60 lbl.configure(width=8)
61 elif column == 'imdbRating':
62 lbl.configure(text='Rating', width=8)
63 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
64
width=12)
65
66 # place in a grid
67 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
68
padx=(0, 10))
69 else:
lbl.grid(row=1, column=j, sticky='we',
70
padx=(0, 1))
71
72 def create_table(self):
73 for i, movie in enumerate(self.movies):
74 for j, key in enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_' +
75
movie['imdbID']
76 if j == 0:
77 # render image
78 self.render_image(movie, i, j, name)
79 else:
80 # print label
81 pass
82
83 def render_image(self, movie, i, j, name):
84 try:
85 # load the image
load = Image .open('images/posters_small/' +
86 movie['imdbID'] + '.jpg')

87 except:
88 # load no image
load =
89
Image.open('images/posters_small/no_image.jpg')
90 finally:
91 render = ImageTk .PhotoImage(load)
lbl_img = tk .Label(self.frame, name=name,
92
image=render, bg=COLORS.ORANGE)
93 lbl_img.image = render
lbl_img.grid(row=i + 2, column=j, padx=(7,
94
0), sticky='we')

In our new form of MovieList class we have new methods. In


the create_page() method, in line 47, we call the
self.create_table() method. As its name implies, it is method is
responsible for creating the table.
In the create_table() method in line 72, we have a nested loop
structure. In the outer loop we iterate over the self.movies list. It is
a list of dictionaries which means every movie in this list is in form
of a dict. Remember that we read csv data to fill this list in the
read_csv() method. In this loop, the loop variable ( i ) represents
the row number of our movie list.
In the inner loop, in line 74, we iterate over the
MovieList.columns which is the column list. The loop variable
( j ) represents the column number of movie list.
In line 75, we create a unique name for each cell in in table as:
name = 'table_row_' + str(i) + str(j) + '_' + movie['imdbID'].
We create this name by concatenating a text 'table_row_' , with
the row and column numbers and the imdbID .
If you examine the imdb_top_250.csv file, you will see that
imdbID is unique for each movie. For example it is tt0111161 for
The Shawshank Redemption. And we get this id as:
movie['imdbID'] . And this imdbID is used as the poster name for
movies. Under the images directory, inside the posters_small
folder, you can see the poster of The Shawshank Redemption
movie with name tt0111161.jpg. This is the same for
posters_large folder too. You can see this image below:

tt0111161.jpg
In line 76, we check if the column number ( j ) is 0 or not as: if
j == 0. This column means the first one and it is the image column.
So we will render the movie poster in this column. To do that we
call another method as render_image as:
self.render_image(movie, i, j, name). As you see, we pass the
movie dictionary, the row number i , the column number j and
unique cell name as the parameters.
In line 83, we have the definition for the render_image()
method. There are some missing posters in the posters_small and
posters_large folders, so we have to handle this case. That’s why
we have a dummy image in each folder as no_image.jpg . We will
render this image, if the movie doesn’t have a poster.
In the try block in line 84, we open the image at the specified
path with the image name as: load =
Image.open('images/posters_small/' + movie['imdbID'] +
'.jpg') . If this line runs successfully, that means this movie has a
poster and the finally block will be executed. In the finally block
we render the image as we did before. (See the render_image()
method in the Home class in cell 29, line 49.) Here we use the
grid() method to place the image at row=i + 2 and column=j . We
also add a padding on the left as padx=(7, 0).
If you run the project now, and click on the Movie List menu
button, you should see the movie images on the right frame as
below. It may take a bit long to display the images because we
don’t have any pagination yet, so it tries to load all of the images at
once. We will fix it later.
Figure 10-24: Movie images rendered

Now let’s print other columns in our movie list, which are:
Rank ( # ), Title , Year , Rating and # of Ratings. Here is the
code for it:

[36]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 import csv
5 from PIL import Image, ImageTk
6 from data.colors import COLORS
7
8 class MovieList:
9 """This class is the frame for movie list."""
10
11 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
12
'imdbRating', 'imdbVotes']
13
def __init__(self, master, bg_color,
14
relief=tk.SUNKEN, side=tk.LEFT):
15 self.frame = tk .Frame(
16 master=master,
17 name='movieList',
18 relief=relief,
19 bg=bg_color
20 )
21 self.side = side
22 self.movies = []
23 self.add_frame()
24
25 def add_frame(self):
26 self.add_page_title('Movie List')
27 self.read_csv()
28 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
29
expand=True)
30
31 def add_page_title(self, title):
32 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
33
fg=COLORS.WHITE,
34 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
35
padx=1,
36 pady=(0, 8), sticky='we')
37
38 def read_csv(self):
39 movie_path = 'data/imdb_top_250.csv'
40 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
41
delimiter=';')
42 for movie in movie_dict:
43 self.movies.append(movie)
44
45 def create_page(self):
46 self.add_header_row()
47 self.create_table()
48
49 def add_header_row(self):
for j, column in
50
enumerate(MovieList.columns):
51 if column != 'imdbID':
lbl = tk .Label(self.frame,
52
text=str(column),
width=54, height=2,
53
bg=COLORS.BLACK,
54 fg=COLORS.WHITE,
55 font=('Arial', 10, 'bold'))
56 # configure width
57 if column == 'Id':
58 lbl.configure(text='#', width=4)
59 elif column == 'Year':
60 lbl.configure(width=8)
61 elif column == 'imdbRating':
62 lbl.configure(text='Rating', width=8)
63 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
64
width=12)
65
66 # place in a grid
67 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
68
padx=(0, 10))
69 else:
lbl.grid(row=1, column=j, sticky='we',
70
padx=(0, 1))
71
72 def create_table(self):
73 for i, movie in enumerate(self.movies):
74 for j, key in enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_' +
75
movie['imdbID']
76 if j == 0:
77 # render image
78 self.render_image(movie, i, j, name)
79 else:
80 # print label
81 self.write_label(movie, i, j, key, name)
82
83 def render_image(self, movie, i, j, name):
84 try:
85 # load the image
load = Image .open('images/posters_small/' +
86
movie['imdbID'] + '.jpg')
87 except:
88 # load no image
load =
89
Image.open('images/posters_small/no_image.jpg')
90 finally:
91 render = ImageTk .PhotoImage(load)
92 lbl_img = tk .Label(self.frame, name=name,
image=render,
93
bg=COLORS.ORANGE)
94 lbl_img.image = render
lbl_img.grid(row=i + 2, column=j, padx=(7,
95
0), sticky='we')
96
97 def write_label(self, movie, i, j, key, name):
lbl = tk .Label(self.frame, name=name,
98
text=str(movie[key]),
height=4, bg=COLORS.WHITE,
99
fg=COLORS.BLACK,
font=('Arial', 10, 'bold'),
100
cursor='hand2')
101
102 # configure width
103 if key == 'Id':
104 lbl.configure(width=4)
105 elif key == 'Title':
106 lbl.configure(width=54, anchor='w')
107 elif key == 'Year':
108 lbl.configure(width=8)
109 elif key == 'imdbRating':
110 lbl.configure(width=8)
111 elif key == 'imdbVotes':
112 lbl.configure(width=12)
113
114 # place in a grid
115 if key == 'imdbVotes':
lbl.grid(row=i + 2, column=j, sticky='we',
116
padx=(0, 10))
117 else:

lbl.grid(row=i + 2, column=j, sticky='we',


118
padx=(0, 1))

We add a new method to our MovieList class to display the


movie data on each row. Which means the columns other than the
first one ( j > 0). The method name is write_label and we call it
in line 81 as: self.write_label(movie, i, j, key, name). We pass
the movie dictionary, the row number i , the column number j , the
column name key , and unique cell name as the parameters.
In line 97 we have the write_label() method. In line 98, it
creates a tk.Label object and name it as lbl . Here is the code: lbl
= tk.Label(self.frame, name=name, text=str(movie[key]),
height=4, bg=COLORS.WHITE, fg=COLORS.BLACK, font=
('Arial', 10, 'bold'), cursor='hand2'). Here, you should
understand how the text parameter works. We pass it as
text=str(movie[key]) . Remember that the movie object is a
dictionary, in which the keys are column names the values are
movie data.
Let’s do a quick debug here. In PyCharm, put a breakpoint on
line 98 and run the application in Debug Mode. The execution will
break on this line. And in the Debug panel at the bottom, click on
the Evaluate Expression button which has the calculator icon as

. This will pop up the Evaluate window. Type movie as the


expression and click Evaluate . You should see the screen below:
Figure 10-25: Debugging the write_label() method

For example, you see the movie dictionary for The


Shawshank Redemption in Figure 10-25. Now let’s understand
the expression of text=str(movie[key]) . We already know that the
key parameter is the column name. So when the column is Title
then the value of the text is ‘The Shawshank Redemption’.
Because movie[‘Title’] returns ‘The Shawshank Redemption’.
This is how we get the text to display in each cell of our table
(movie list).
The rest of the write_label() method is very similar to the
add_header_row() method. We configure the width of each cell
and then we place it with the grid() function. In the grid()
function, in lines 116 and 118, be careful that the row numbers are
given as row=i + 2. Here +2 is for the page title and the header
row.
Now if you run the project you should see a nice movie list
table on the screen as below (which again may take some time):

Figure 10-26: Movie list table with movies data in it

Movie List - Part 5


Our movie list looks good but there is a serious performance
problem with it, when we run the application. The reason is that, it
tries to load all of the 250 images at once. In this sub-section we
will fix this bug by applying pagination to our list. We will render
the list in pages and there will be only 10 movies at each page. This
means we have to wait for loading time for just 10 movies. We will
also add some new functionalities too.

[37]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 from tkinter import ttk
5 import csv
6 from PIL import Image, ImageTk
7 from data.colors import COLORS
8
9 class MovieList:
10 """This class is the frame for movie list."""
11
12 # pagination variables
13 page_number = 1
14 movies_per_page = 10
15 total_num_pages = 0
16
17 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
18
'imdbRating', 'imdbVotes']
19
def __init__(self, master, bg_color,
20
relief=tk.SUNKEN, side=tk.LEFT):
21 self.frame = tk .Frame(
22 master=master,
23 name='movieList',
24 relief=relief,
25 bg=bg_color
26 )
27 self.side = side
28 self.movies = []
29 self.add_frame()
30
31 def add_frame(self):
32 self.add_page_title('Movie List')
33 self.read_csv()
34 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
35
expand=True)
36
37 def add_page_title(self, title):
38 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
39
fg=COLORS.WHITE,
40 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
41
padx=1,
42 pady=(0, 8), sticky='we')
43
44 def read_csv(self):
45 movie_path = 'data/imdb_top_250.csv'
46 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
47
delimiter=';')
48 for movie in movie_dict:
49 self.movies.append(movie)
50
51 # assign total_num_pages
MovieList.total_num_pages = len(self.movies)
52 // MovieList .movies_per_page + 1
53
54 def create_page(self):
55 self.add_header_row()
56 self.create_table()
57 self.create_combo_box()
58
59 def add_header_row(self):
for j, column in
60
enumerate(MovieList.columns):
61 if column != 'imdbID':
lbl = tk .Label(self.frame,
62
text=str(column),
width=54, height=2,
63
bg=COLORS.BLACK,
64 fg=COLORS.WHITE,
65 font=('Arial', 10, 'bold'))
66 # configure width
67 if column == 'Id':
68 lbl.configure(text='#', width=4)
69 elif column == 'Year':
70 lbl.configure(width=8)
71 elif column == 'imdbRating':
72 lbl.configure(text='Rating', width=8)
73 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
74
width=12)
75
76 # place in a grid
77 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
78
padx=(0, 10))
79 else:

lbl.grid(row=1, column=j, sticky='we',


80 padx=(0, 1))
81
82 def create_table(self):
83 for i, movie in enumerate(self.movies):
84 # if i = 2
85 # 10 <= i < 20
start_page_num = (MovieList .page_number -
86
1) * MovieList .movies_per_page
end_page_num = MovieList .page_number *
87
MovieList.movies_per_page
88 if start_page_num <= i < end_page_num:
for j, key in
89
enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_'
90
+ movie[ 'imdbID']
91 if j == 0:
92 # render image
93 self.render_image(movie, i, j, name)
94 else:
95 # print label
96 self.write_label(movie, i, j, key, name)
97
98 # keep the last row
99 self.i = i + 3
100
101 def render_image(self, movie, i, j, name):
102 try:
103 # load the image
load = Image .open('images/posters_small/' +
104
movie['imdbID'] + '.jpg')
105 except:
106 # load no image
load =
107
Image.open('images/posters_small/no_image.jpg')
108 finally:
109 render = ImageTk .PhotoImage(load)
110 lbl_img = tk .Label(self.frame, name=name,
image=render,
111
bg=COLORS.ORANGE)
112 lbl_img.image = render
lbl_img.grid(row=i + 2, column=j, padx=(7,
113
0), sticky='we')
114
115 def write_label(self, movie, i, j, key, name):
lbl = tk .Label(self.frame, name=name,
116
text=str(movie[key]),
height=4, bg=COLORS.WHITE,
117
fg=COLORS.BLACK,
font=('Arial', 10, 'bold'),
118
cursor='hand2')
119
120 # configure width
121 if key == 'Id':
122 lbl.configure(width=4)
123 elif key == 'Title':
124 lbl.configure(width=54, anchor='w')
125 elif key == 'Year':
126 lbl.configure(width=8)
127 elif key == 'imdbRating':
128 lbl.configure(width=8)
129 elif key == 'imdbVotes':
130 lbl.configure(width=12)
131
132 # zebra effect
133 self.fill_bg(lbl, i)

134
135 # place in a grid
136 if key == 'imdbVotes':
lbl.grid(row=i + 2, column=j, sticky='we',
137
padx=(0, 10))
138 else:
lbl.grid(row=i + 2, column=j, sticky='we',
139
padx=(0, 1))
140
141 def fill_bg(self, widget, i):
142 # check if the row (i) is odd or even
143 if i % 2 == 1:
144
widget.configure(bg=COLORS.LIST_ODD_LINE)
145 else:
146
widget.configure(bg=COLORS.LIST_EVEN_LINE)
147
148 def create_combo_box_select_event(self, event):
149 pass
150
151 def create_combo_box(self):
152 # each page -> 10 movies
153 # total 250 movies
154 # to upper limit 250 / 10 = 25
values = list(range(1,
155
MovieList.total_num_pages))
pages = ttk .Combobox(self.frame,
156
values=values, width=4)
157 # index of the page (1 minus page_number)
158 pages.current(MovieList.page_number - 1)
159
160 # bind select event
pages.bind('<<ComboboxSelected>>',
161
self.create_combo_box_select_event)
pages.grid(row=self.i, column=2, pady=(15,
162
0))

In cell 37, we add the pagination property to our MovieList


and also a good looking zebra effect.
Let’s start with the zebra effect. It means odd numbered rows
having different background color than the even numbered rows.
We define a method for this as fill_bg and we call it on line 133,
inside the write_label() method. In line 141, you see the
fill_bg() method. It simply checks if the row number ( i ) is odd or
even and configures different background colors for each case.

Pagination Variables:
In the top line of the class definition, we have new attributes on
lines 13 to 15. These are the pagination variables which are class
level (class attributes) and here are their definitions:
page_number : stores the current page number which is
displayed on the screen
movies_per_page : this constant defines how many movies we
have on the screen (10 here)
total_num_pages : keeps the total number of pages that will
be displayed
We will use these attributes to set a proper pagination system in
our movie list.

In the read_csv() method in line 52, we calculate and assign


the value of total_num_pages . Here is the code:
MovieList.total_num_pages = len(self.movies) //
MovieList.movies_per_page + 1. We first get the length of the
self.movies list, then we do an integer division by dividing it to
MovieList.movies_per_page . And finally we increase the result
by 1 , because we want the first page number to be 1 , not 0 .
In the create_page() , in line 57, we call a new method which
is create_combo_box . This method will create a combo box to be
able to change the current page number.

Which movies to display?


In the create_table() method we modify the nested for loops.
We want to render only a limited number of movies on each page.
This number is movies_per_page and its value is 10 in our
project. But this is not enough to render the respective movies on
the current page. We don’t know which 10 movies they are. More
specifically, on each page, we need to know where to start and
where to stop. So we define two variables for this purpose which
are start_page_num and end_page_num : Here they are:
start_page_num = (MovieList.page_number - 1) *
MovieList.movies_per_page
end_page_num = MovieList.page_number *
MovieList.movies_per_page .
We will render every movie which has rank ( # ) number in this
range. That’s why we put an if condition before the inner loop.
Here is the condition: if start_page_num <= i < end_page_num.
And that guarantees that only movies_per_page (which is 10)
movies will be rendered.
In the create_table() method in line 99, we define a new
attribute as: self.i = i + 3. This self.i attribute keeps the last
available row after the table is rendered. Here i is the last value in
the for loop and +3 is for page title, header row and an empty
row.
In line 151, you see the definition of the create_combo_box()
method. In line 155, we define a list called values , which includes
a range of numbers as: values = list(range(1,
MovieList.total_num_pages)) . The range is simply from 1 to the
total_num_pages .
In line 156, we create a Combobox which is a widget that
exists in a sub-package of Tkinter. The name of this sub-package is
ttk and we import it in line 4. We name our Combobox as pages ,
because it’s going to hold the page numbers. Here is the code:
pages = ttk.Combobox(self.frame, values=values, width=4).
In line 158, we set the selected element of the pages
Combobox with the current() method as:
pages.current(MovieList.page_number - 1). The current
method works with indices so we pass MovieList.page_number -
1 as the current index .
In line 161, we bind the select event of our Combobox as:
pages.bind('<<ComboboxSelected>>',
self.create_combo_box_select_event) . Binding gives us the
selected index when the user changes the value in the Combobox.
The event name is '<<ComboboxSelected>>' and we pass a
method as the event handler. Our event handler is going to be
create_combo_box_select_event() method which we will define
next.
In line 162, we put the pages Combobox on the screen with the
help of the grid() method as: pages.grid(row=self.i, column=2,
pady=(15, 0)). Here you see self.i as the row number.
In line 148, you see the create_combo_box_select_event()
method. We don’t have any functionality yet so we just type pass
in it. We will define it later.
Now if we run the project we will see a Combobox below the
screen as follows:
Figure 10-27: Combobox and pagination

Movie List - Part 6


In the last sub-section we started to build pagination on our list.
As you see in Figure 10-27, we can display only 10 movies on the
screen. These are the first 10 movies and we cannot change them.
Our Combobox is not working yet. In this sub-section we will add
this functionality and we will be able to navigate between pages by
changing the selected value in our Combobox.

[38]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 from tkinter import ttk
5 import csv
6 from PIL import Image, ImageTk
7 from data.colors import COLORS
8
9 class MovieList:
10 """This class is the frame for movie list."""
11
12 # pagination variables
13 page_number = 1
14 movies_per_page = 10
15 total_num_pages = 0
16
17 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
18
'imdbRating', 'imdbVotes']
19
def __init__(self, master, bg_color,
20
relief=tk.SUNKEN, side=tk.LEFT):
21 self.frame = tk .Frame(
22 master=master,
23 name='movieList',
24 relief=relief,
25 bg=bg_color
26 )
27 self.side = side
28 self.movies = []
29 self.add_frame()
30
31 def add_frame(self):
32 self.add_page_title('Movie List')
33 self.read_csv()
34 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
35
expand=True)
36
37 def add_page_title(self, title):
38 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
39
fg=COLORS.WHITE,
40 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
41
padx=1,
42 pady=(0, 8), sticky='we')
43
44 def read_csv(self):
45 movie_path = 'data/imdb_top_250.csv'
46 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
47
delimiter=';')
48 for movie in movie_dict:
49 self.movies.append(movie)
50
51 # assign total_num_pages
MovieList.total_num_pages = len(self.movies)
52
// MovieList .movies_per_page + 1
53
54 def create_page(self):
55 self.add_header_row()
56 self.create_table()
57 self.create_combo_box()
58
59 def add_header_row(self):

for j, column in
60 enumerate(MovieList.columns):
61 if column != 'imdbID':
lbl = tk .Label(self.frame,
62
text=str(column),
width=54, height=2,
63
bg=COLORS.BLACK,
64 fg=COLORS.WHITE,
65 font=('Arial', 10, 'bold'))
66 # configure width
67 if column == 'Id':
68 lbl.configure(text='#', width=4)
69 elif column == 'Year':
70 lbl.configure(width=8)
71 elif column == 'imdbRating':
72 lbl.configure(text='Rating', width=8)
73 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
74
width=12)
75
76 # place in a grid
77 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
78
padx=(0, 10))
79 else:
lbl.grid(row=1, column=j, sticky='we',
80
padx=(0, 1))
81
82 def create_table(self):
83 for i, movie in enumerate(self.movies):
84 # if i = 2
85 # 10 <= i < 20
start_page_num = (MovieList .page_number -
86 1) * MovieList .movies_per_page
end_page_num = MovieList .page_number *
87
MovieList.movies_per_page
88 if start_page_num <= i < end_page_num:
for j, key in
89
enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_'
90
+ movie[ 'imdbID']
91 if j == 0:
92 # render image
93 self.render_image(movie, i, j, name)
94 else:
95 # print label
96 self.write_label(movie, i, j, key, name)
97
98 # keep the last row
99 self.i = i + 3
100
101 def render_image(self, movie, i, j, name):
102 try:
103 # load the image
load = Image .open('images/posters_small/' +
104
movie['imdbID'] + '.jpg')
105 except:
106 # load no image
load =
107
Image.open('images/posters_small/no_image.jpg')
108 finally:
109 render = ImageTk .PhotoImage(load)
110 lbl_img = tk .Label(self.frame, name=name,
image=render,
111
bg=COLORS.ORANGE)
112 lbl_img.image = render
113 lbl_img.grid(row=i + 2, column=j, padx=(7,
0), sticky='we')
114
115 def write_label(self, movie, i, j, key, name):
lbl = tk .Label(self.frame, name=name,
116
text=str(movie[key]),
height=4, bg=COLORS.WHITE,
117
fg=COLORS.BLACK,
font=('Arial', 10, 'bold'),
118
cursor='hand2')
119
120 # configure width
121 if key == 'Id':
122 lbl.configure(width=4)
123 elif key == 'Title':
124 lbl.configure(width=54, anchor='w')
125 elif key == 'Year':
126 lbl.configure(width=8)
127 elif key == 'imdbRating':
128 lbl.configure(width=8)
129 elif key == 'imdbVotes':
130 lbl.configure(width=12)
131
132 # zebra effect
133 self.fill_bg(lbl, i)
134
135 # place in a grid
136 if key == 'imdbVotes':
lbl.grid(row=i + 2, column=j, sticky='we',
137
padx=(0, 10))
138 else:
lbl.grid(row=i + 2, column=j, sticky='we',
139
padx=(0, 1))
140
141 def fill_bg(self, widget, i):
142 # check if the row (i) is odd or even
143 if i % 2 == 1:
144
widget.configure(bg=COLORS.LIST_ODD_LINE)
145 else:
146
widget.configure(bg=COLORS.LIST_EVEN_LINE)
147
148 def create_combo_box_select_event(self, event):
MovieList.page_number =
149
int(event.widget.get())
150 # clear the table cells
151 self.clear_table(event)
152 # recreate the table
153 self.create_table()
154
155 def create_combo_box(self):
156 # each page -> 10 movies
157 # total 250 movies
158 # to upper limit 250 / 10 = 25
values = list(range(1,
159
MovieList.total_num_pages))
pages = ttk .Combobox(self.frame,
160
values=values, width=4)
161 # index of the page (1 minus page_number)
162 pages.current(MovieList.page_number - 1)
163
164 # bind select event
pages.bind('<<ComboboxSelected>>',
165
self.create_combo_box_select_event)
pages.grid(row=self.i, column=2, pady=(15,
166
0))
167
168 def clear_table(self, event):
169 master = event .widget.master
170 # loop over the children
171 master_children_copy = master .children.copy()
172 for child in master_children_copy:
173 if 'table_row_' in child:
174 master.children[child].destroy()

In cell 38, we have the new form of our MovieList class. We


modified the create_combo_box_select_event() method in line
148.
Inside this method, in line 149, we get the selected element of
the Combobox as : event.widget.get(). And we convert this value
to integer and then assign it to the MovieList.page_number
attribute. Here is the code: MovieList.page_number =
int(event.widget.get()) . Now the current page number,
MovieList.page_number , is the selected item of the Combobox.
In line 151, we call a new method as self.clear_table(event) .
This method is going to clear the cells in the table.
In line 168, we have the clear_table() method. It first get the
master of the widget that fires the event. This widget is the
Combobox and its master is the self.frame of our MovieList
class.
In line 171, we first copy the children in the master as:
master_children_copy = master.children.copy(). Then in line
172, we iterate over this dictionary. And in the for loop, we check
if the name this current child widget starts with 'table_row_' . If
it starts with this text, then it means that, this child is a movie
label. So we will destroy it as: master.children[child].destroy() .
Be careful that we only want to destroy the labels that contain
movie data. Remember that we set this name in the create_table()
method as name = 'table_row_' + str(i) + str(j) + '_' +
movie['imdbID'] .
In the create_combo_box_select_event() method after we
clear the table content we call self.create_table() in line 153.
This is how we create the new cells in our table. Remember that
create_table() uses the MovieList.page_number to create the
items, which we set to the selected element of the Combobox.
And that’s it, if you run the code now, you will see that the
pagination works perfect. For example, in the image below we
select the 6th page:
Figure 10-28: Pagination completed

Bug Fixing in the LeftFrame


When we start the application it displays the home page on the
right frame. But on the left frame none of the buttons seem to be
selected. Actually the Home button should display as selected
because we see the home page on the screen at start. This is a bug
and we need to fix it. When the app starts, you will see the Home
menu button as the selected one.
To fix this bug, we need to modify the add_menus() method
in the LeftFrame . Let’s do it:
[39]: 1 # widget/left_frame/LeftFrame.py file
2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 # circular import error
8 # from widget import Button
9
10 # exact import
11 from widget.button.Button import Button
from widget.right_frame.RightFrame import
12
RightFrame
13
14 class LeftFrame:
15 """
16 Class for Left Frame to hold the menu items.
17 """
18
19 def __init__(self, window, name):
20 self.frame = tk .Frame(
21 master=window,
22 name=name,
23 bg=COLORS.BLACK
24 )
25 self.master = window
26 self.add_frame()
27 self.add_menus()
28
29 def add_frame(self):
30 self.frame.pack(side=tk.LEFT, fill=tk.Y,
31 pady=(62, 0))
32
# method for click event of menu buttons ->
33
event handler
34 def handle_click(self, event):
35 self.manage_button_colors(event)
36 page_name = str(event.widget).split('.')[2]
37
38 # self is left frame -> master = root
39 # right frame is in children of master
40 rightFrame = self.master.children['rightFrame']
41
42 # destroy children
43 RightFrame.destroy_children(rightFrame)
44
45 # add new page -> page_name
RightFrame.frame_content(rightFrame,
46
page_name)
47
48 def add_menus(self):
49 # add menus in loop
50 for menu_key, menu_text in MENU .items():
51 if menu_key == 'about':
button = Button( self.frame, menu_key,
52
menu_text,
COLORS.ORANGE,
53
COLORS.BLACK,
54 18, 2,
55 handle_click=self.handle_click,
56 side=tk.BOTTOM)
57 else:
button = Button( self.frame, menu_key,
58 menu_text,
59 COLORS.ORANGE,
COLORS.BLACK,
60 18, 2,
61 handle_click=self.handle_click,
62 side=tk.TOP)
63
64 if menu_key == 'home':
65
self.selected_button_color(button.button)
66
67 def manage_button_colors(self, event):
68 # clicked button -> event.widget
# all the menu buttons ->
69
event.widget.master.children
for child in
70
event.widget.master.winfo_children():
71 if child == event .widget:
child.configure(bg=COLORS.ORANGE,
72
fg=COLORS.WHITE)
73 else:
child.configure(bg=COLORS.BLACK,
74
fg=COLORS.ORANGE)
75
76 def selected_button_color(self, button):
77 # tk button configure
button.configure(bg=COLORS.ORANGE,
78
fg=COLORS.WHITE)

In cell 39, you see the new form of our LeftFrame class. We
add a new method as selected_button_color . This method will be
responsible for modifying the Home menu button.
In the add_menus() method, in line 64, we check the
menu_key as: menu_key == 'home'. If the menu_key is
‘home’ then we call the selected_button_color() method as:
self.selected_button_color(button.button) . The parameter we
pass is button.button because the button object we create in line
58 is our custom wrapper Button class. And it has an attribute
called self.button which is the tk.Button. That’s why we pass it as
button.button . Because the selected_button_color() method
expects a tk.Button to configure it.
In line 76, you see the definition of the
selected_button_color() method. It takes a tk.Button object and
configures its bg and fg as:
button.configure(bg=COLORS.ORANGE,
fg=COLORS.WHITE) .
And that’s it, now when you app starts, the Home menu button
will display as selected by default. See the image below:
Figure 1-29: Home menu button selected when the app starts

Movie Detail - Part 1


Now it’s time to develop the Movie Detail page. When the user
clicks on a movie in the Movie List, we want to display the details
of that movie in a new page. This new page will be the
MovieDetail class and the user will be navigated to that page on
click.
Let’s start by creating a new folder named movie_detail in the
page package. And in this movie_detail directory create a new
Python file as MovieDetail.py . The file name and the class name
are again the same. It’s going to be a large class so we will define it
in two sub-sections. Here is the initial form of the MovieDetail
class:

[40]: 1 import tkinter as tk


2 from data.colors import COLORS
3
4 class MovieDetail:
5 """Page to hold movie detail."""
6
def __init__(self, window, bg_color,
7
imdbID=None,
8 movies=[], relief=tk.SUNKEN,
9 side=tk.LEFT):
10 self.frame = tk .Frame(
11 master=window,
12 name='movieDetail',
13 relief=relief,
14 bg=bg_color
15 )
16 self.side = side
17 self.imdbID = imdbID
18 self.movies = movies
19 self.add_frame()
20
21 def add_frame(self):
22 self.add_content()
23 self.add_page_title('Movie Detail')
24 self.frame.pack(side=self.side, fill=tk.BOTH,
25 expand=True)
26
27 def add_page_title(self, title):
28 lbl = tk .Label(self.frame, text=title, height=3,
29 bg=COLORS.BLACK,
fg=COLORS.WHITE,
30 font=('Arial', 12, 'bold'))
31 lbl.pack(fill=tk.BOTH, padx=(1, 0))
32
33 def add_content(self):
34 pass

In cell 40, we have the MovieDetail class definition. It creates


a tk.Frame and names it as self.frame . Here are the important
parameters in the __init__() method: window , bg_color ,
imdbID =None, movies =[]. The imdbID parameter is needed to
decide which movie to display. And the movies list is simply all
the movies data. From which we will filter the movie with the
current imdbID .
In the add_frame() method we call add_content() and
add_page_title() methods. We skip the add_content() method
for the time being but we add the page title in line 28.
Let’s add the MovieDetail class to the __init__.py file of the
page package:

[41]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail
6 """
7
8 from .home.Home import Home
9 from .movie_list.MovieList import MovieList
from .movie_detail.MovieDetail import
10 MovieDetail
Now we need to handle the click event in the MovieList class.
Because that’s where the user clicks on the movies. Here is the
modified form of the MovieList class including the click
functionality:

[42]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 from tkinter import ttk
5 import csv
6 from PIL import Image, ImageTk
7 from data.colors import COLORS
from page.movie_detail.MovieDetail import
8
MovieDetail
9
10 class MovieList:
11 """This class is the frame for movie list."""
12
13 # pagination variables
14 page_number = 1
15 movies_per_page = 10
16 total_num_pages = 0
17
18 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
19
'imdbRating', 'imdbVotes']
20
def __init__(self, master, bg_color,
21
relief=tk.SUNKEN, side=tk.LEFT):
22 self.frame = tk .Frame(
23 master=master,
24 name='movieList',
25 relief=relief,
26 bg=bg_color
27 )
28 self.side = side
29 self.movies = []
30 self.add_frame()
31
32 def add_frame(self):
33 self.add_page_title('Movie List')
34 self.read_csv()
35 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
36
expand=True)
37
38 def add_page_title(self, title):
39 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
40
fg=COLORS.WHITE,
41 font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
42
padx=1,
43 pady=(0, 8), sticky='we')
44
45 def read_csv(self):
46 movie_path = 'data/imdb_top_250.csv'
47 with open(movie_path, 'r') as file:
movie_dict = csv .DictReader(file,
48
delimiter=';')
49 for movie in movie_dict:
50 self.movies.append(movie)
51
52 # assign total_num_pages
MovieList.total_num_pages = len(self.movies)
53
// MovieList .movies_per_page + 1
54
55 def create_page(self):
56 self.add_header_row()
57 self.create_table()
58 self.create_combo_box()
59
60 def add_header_row(self):
for j, column in
61
enumerate(MovieList.columns):
62 if column != 'imdbID':
lbl = tk .Label(self.frame,
63
text=str(column),
width=54, height=2,
64
bg=COLORS.BLACK,
65 fg=COLORS.WHITE,
66 font=('Arial', 10, 'bold'))
67 # configure width
68 if column == 'Id':
69 lbl.configure(text='#', width=4)
70 elif column == 'Year':
71 lbl.configure(width=8)
72 elif column == 'imdbRating':
73 lbl.configure(text='Rating', width=8)
74 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
75
width=12)
76
77 # place in a grid
78 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
79
padx=(0, 10))
80 else:
lbl.grid(row=1, column=j, sticky='we',
81
padx=(0, 1))
82
83 def create_table(self):
84 for i, movie in enumerate(self.movies):
85 # if i = 2
86 # 10 <= i < 20
start_page_num = (MovieList .page_number -
87
1) * MovieList .movies_per_page
end_page_num = MovieList .page_number *
88
MovieList.movies_per_page
89 if start_page_num <= i < end_page_num:
for j, key in
90
enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_'
91
+ movie[ 'imdbID']
92 if j == 0:
93 # render image
94 self.render_image(movie, i, j, name)
95 else:
96 # print label
97 self.write_label(movie, i, j, key, name)
98
99 # keep the last row
100 self.i = i + 3
101
102 def render_image(self, movie, i, j, name):
103 try:
104 # load the image
load = Image .open('images/posters_small/' +
105
movie['imdbID'] + '.jpg')
106 except:
107 # load no image
load =
108
Image.open('images/posters_small/no_image.jpg')
109 finally:
110 render = ImageTk .PhotoImage(load)
111 lbl_img = tk .Label(self.frame, name=name,
image=render,
112
bg=COLORS.ORANGE)
113 lbl_img.image = render
lbl_img.grid(row=i + 2, column=j, padx=(7,
114
0), sticky='we')
115
116 def write_label(self, movie, i, j, key, name):
lbl = tk .Label(self.frame, name=name,
117
text=str(movie[key]),
height=4, bg=COLORS.WHITE,
118
fg=COLORS.BLACK,
font=('Arial', 10, 'bold'),
119
cursor='hand2')
120
121 # bind the left click event
122 lbl.bind('<Button-1>', self.movie_click)
123
124 # configure width
125 if key == 'Id':
126 lbl.configure(width=4)
127 elif key == 'Title':
128 lbl.configure(width=54, anchor='w')
129 elif key == 'Year':
130 lbl.configure(width=8)
131 elif key == 'imdbRating':
132 lbl.configure(width=8)
133 elif key == 'imdbVotes':
134 lbl.configure(width=12)
135
136 # zebra effect
137 self.fill_bg(lbl, i)
138
139 # place in a grid
140 if key == 'imdbVotes':
lbl.grid(row=i + 2, column=j, sticky='we',
141
padx=(0, 10))
142 else:
lbl.grid(row=i + 2, column=j, sticky='we',
143
padx=(0, 1))
144
145 def fill_bg(self, widget, i):
146 # check if the row (i) is odd or even
147 if i % 2 == 1:
148
widget.configure(bg=COLORS.LIST_ODD_LINE)
149 else:
150
widget.configure(bg=COLORS.LIST_EVEN_LINE)
151
152 def create_combo_box_select_event(self, event):
MovieList.page_number =
153
int(event.widget.get())
154 # clear the table cells
155 self.clear_table(event)
156 # recreate the table
157 self.create_table()
158
159 def create_combo_box(self):
160 # each page -> 10 movies
161 # total 250 movies
162 # to upper limit 250 / 10 = 25
values = list(range(1,
163 MovieList.total_num_pages))

pages = ttk .Combobox(self.frame,


164
values=values, width=4)
165 # index of the page (1 minus page_number)
166 pages.current(MovieList.page_number - 1)
167
168 # bind select event
pages.bind('<<ComboboxSelected>>',
169
self.create_combo_box_select_event)
pages.grid(row=self.i, column=2, pady=(15,
170
0))
171
172 def clear_table(self, event):
173 master = event .widget.master
174 # loop over the children
175 master_children_copy = master .children.copy()
176 for child in master_children_copy:
177 if 'table_row_' in child:
178 master.children[child].destroy()
179
180 def movie_click(self, event):
181 # get the imdbID from the widget name
182 imdbID = str(event.widget).split('_')[3]
183
# modify the right frame (clear the right
184
frame)
185 self.modify_right_frame(event, imdbID)
186
187 # modify the left frame
188 self.modify_left_frame(event)
189
190 def modify_right_frame(self, event, imdbID):
191 rightFrame = event .widget.master
192 # loop over the children of rightframe
193 for child in rightFrame .winfo_children():
194 child.destroy()
195
196 # add Movie Detail
197 MovieDetail(self.frame, COLORS.ORANGE,
198 imdbID=imdbID, movies=self.movies)
199
200 def modify_left_frame(self, event):
201 # get the root element
202 root = event .widget.master.master.master
203 for child in root .winfo_children():
204 if str(child) == '.leftFrame':
205 for ch in child .winfo_children():
206 if str(ch) == '.leftFrame.movieDetail':
ch.configure(bg=COLORS.ORANGE,
207
fg=COLORS.WHITE)
208 else:
ch.configure(bg=COLORS.BLACK,
209
fg=COLORS.ORANGE)

In cell 42, we add the click functionality the movie list. Inside
the write_label() method in line 122, we bind the left click event
to the lbl object as: lbl.bind('<Button-1>', self.movie_click).
Here, '<Button-1>' means left mouse click and self.movie_click
is the event handler we pass to the bind() function.
In line 180, we have the definition of the movie_click()
method. It takes the event as the parameter and in line 182, it
extracts the imdbID from the name of the widget that sends the
event. Here is the code: imdbID = str(event.widget).split('_')
[3] . Remember that in the create_table() method we concatenated
the imdbID to the end of the widget name as: name =
'table_row_' + str(i) + str(j) + '_' + movie['imdbID']. And
here we extract the imdbID .
In line 185, we call a new method, modify_right_frame , by
passing the event and the imdbID . This method is going to
modify the RightFrame .
In line 188, we call another method, modify_left_frame , by
passing the event . This one will modify the LeftFrame .
In line 190, we have this modify_right_frame() method. We
first get the rightFrame object as: rightFrame =
event.widget.master . Then we loop over the children of the
rightFrame and destroy them. This is needed because we want to
clear the current content in the right frame. We will fill it with
MovieDetail in a minute. And in line 197, we call the
MovieDetail class and pass the parameters as:
MovieDetail(self.frame, COLORS.ORANGE,
imdbID=imdbID, movies=self.movies).
In line 200, we have the modify_left_frame() method. The
main idea for this method is to get all the menu buttons and change
their configurations. At the end, it will make the Movie Detail
button as selected.
In line 202, it gets the root window as: root =
event.widget.master.master.master . And in line 203, it loops
over the children of this root object. It finds the LeftFrame ( ch )
in line 204 as: str(child) == '.leftFrame'. Then it loops over the
children of the left frame. Finally it finds the MovieDetail child in
line 206 as: if str(ch) == '.leftFrame.movieDetail'. And it
changes the bg and fg colors of the LeftFrame as selected. It
configures the rest as unselected.
And here is the result when we run the app at this stage and
click any of the movies in the Movie List page:

Figure 10-30: Empty Movie Detail page when a movie is clicked

Movie Detail - Part 2


Now let’s finalize our Movie Detail page and make ready to
display the details of the selected movie. Here is the final code of
MovieDetail class:

[43]: 1 import tkinter as tk


2 from data.colors import COLORS
3 from PIL import Image, ImageTk
4
5 class MovieDetail:
6 """Page to hold movie detail."""
7
def __init__(self, window, bg_color,
8
imdbID=None,
9 movies=[], relief=tk.SUNKEN,
10 side=tk.LEFT):
11 self.frame = tk .Frame(
12 master=window,
13 name='movieDetail',
14 relief=relief,
15 bg=bg_color
16 )
17 self.side = side
18 self.imdbID = imdbID
19 self.movies = movies
20 self.add_frame()
21
22 def add_frame(self):
23 self.add_content()
24 self.add_page_title('Movie Detail')
25 self.frame.pack(side=self.side, fill=tk.BOTH,
26 expand=True)
27
28 def add_page_title(self, title):
29 if self.imdbID != None:
lbl = tk .Label(self.frame,
30
text=self.movie['Title'], height=3,
bg=COLORS.BLACK,
31 fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
32 padx=1, pady=(0, 8), sticky='we')
33 else:
lbl = tk .Label(self.frame, text=title,
34
height=3,
bg=COLORS.BLACK,
35
fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
36 lbl.pack(fill=tk.BOTH, padx=(1, 0))
37
38 def add_content(self):
39 if self.imdbID != None:
40 self.render_image()
41 self.get_movie()
42 self.render_keys()
43 self.render_values()
44 else:
45 pass
46
47 def render_image(self):
48 try:
49 # load the image
load = Image .open('images/posters_large/' +
50
str(self.imdbID) + '.jpg')
51 except:
52 # load no image
load =
53
Image.open('images/posters_large/no_image.jpg')
54 finally:
55 render = ImageTk .PhotoImage(load)
lbl_img = tk .Label(self.frame, image=render,
56
bg=COLORS.ORANGE)
57 lbl_img.image = render

lbl_img.grid(row=1, column=0,
58 columnspan=2, pady=(0, 10))
59
60 def get_movie(self):
61 for m in self.movies:
62 if m[ 'imdbID'] == self.imdbID:
63 self.movie = m
64 break
65
66 def render_keys(self):
67 for i, key in enumerate(self.movie.keys()):
68 txt = str(key)
lbl = tk .Label(self.frame, text=txt, height=2,
69
width=12, anchor='w')
70 self.fill_bg(lbl, i)
71 lbl.grid(row=i + 2, column=0, padx=(10, 1))
72
73 def fill_bg(self, widget, i):
74 # check if the row (i) is odd
75 if i % 2 == 1:
76
widget.configure(bg=COLORS.LIST_ODD_LINE)
77 else:
78
widget.configure(bg=COLORS.LIST_EVEN_LINE)
79
80 def render_values(self):
81 for i, value in enumerate(self.movie.values()):
82 txt = str(value)
lbl = tk .Label(self.frame, text=txt, height=2,
83
width=96, anchor='w')
84 self.fill_bg(lbl, i)
85 lbl.grid(row=i + 2, column=1, padx=(0, 8))
In cell 43, you see the final form of the MovieDetail class.
Let’s see what has been modified:
In the add_page_title() method in line 28, we first check if the
self.imdbID is None or not. If it’s not None , then we get the title
of the selected movie as self.movie['Title'] and pass it to the text
parameter as: text=self.movie['Title'] . We will see how set the
self.movie attribute in a minute.

self.imdbID != None:
What does it mean, when we say self.imdbID != None. This
means that this class has been called by a click event on a movie.
We know this, because the imdbID for the movie is not empty.
Remember that we got this parameter in the __init__() method.
If self.imdbID is equal to None , this means that no movies are
selected. And the MovieDetail page is called by just clicking on
the menu button (Movie Detail) on the left of the screen. In other
words, it has been rendered as an empty page from the
RightFrame . We will modify the RightFrame for this soon.
In the add_content() method in line 38, we first check if the
imdbID is not None as: if self.imdbID != None. If it’s not
None , then we can render the movie details. So, we call the
necessary methods which are: render_image() , get_movie() ,
render_keys() and render_values() .
In the render_image() method we load the big image for the
selected movie. Remember that we have the path that contains the
imdbID as the file name. But this time we the directory is
images/posters_large .
In the get_movie() method, we just filter the selected movie
from the self.movies list. The value of self.movies comes from
the movies parameter. Then in line 62, we filter the one with the
current as: if m['imdbID'] == self.imdbID. And we set this one
to the self.movie attribute. That’s how we get the selected movie
data.
In the render_keys() method in line 66, we simply render the
keys, which are the column names like; Id, Title, Year, Runtime,
Genre, Director etc. This is the first column of our details table,
column=0 . We also call the fill_bg() method in line 70, to create
a zebra effect on the keys.
In the render_values() method in line 80, we render the values
of the self.movie dictionary. Remember that the actual movie data
is stored as values in the movie object. Again we call the fill_bg()
method to create the same zebra effect on the values.
And we finished defining the MovieDetail class. As the last
thing we will modify the RightFrame class to call the
MovieDetail when the user clicks on the menu Movie Detail
button on the left. Here it is:

[44]: 1 # widget/right_frame/RightFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 from page import Home, MovieList, MovieDetail
7
8 class RightFrame:
9 """It will hold the pages on the right frame."""
10
11 # class attribute
12 bg_color = COLORS .ORANGE
13
def __init__(self, window, name,
14 relief=tk.SUNKEN, side=tk.LEFT):

15 self.frame = tk .Frame(
16 master=window,
17 name=name,
18 relief=relief,
19 bg=RightFrame.bg_color
20 )
21 self.side = side
22 self.add_frame()
23
24 def add_frame(self):
25 # frame content
26 self.frame_content()
self.frame.pack(side=self.side, fill=tk.BOTH,
27
expand=True)
28
29 def frame_content(self, page_name='home'):
30 try:
31 incoming_frame = self.frame
32 except:
33 incoming_frame = self
34 finally:
35 if page_name == 'home':
36 # add home page
Home(incoming_frame,
37
RightFrame.bg_color)
38 elif page_name == 'movieList':
39 # add movie list page
MovieList(incoming_frame,
40
RightFrame.bg_color)
41 elif page_name == 'movieDetail':
42 # add movie detail page
MovieDetail(incoming_frame,
43
RightFrame.bg_color)
44
# static method -> Class Method (no self
45
parameter)
46 def destroy_children(frame):
47 # destroy the children
48 for child in frame .winfo_children():
49 child.destroy()

In cell 44, we have the final form of the RightFrame class.


Remember that it renders the pages in its frame_content() method
in line 29. We add an elif part for the MovieDetail class as: elif
page_name == 'movieDetail'. Now if the page_name is
'movieDetail' , then it calls the class as:
MovieDetail(incoming_frame, RightFrame.bg_color). As you
see there is no imdbID in this call. That’s how we render an empty
movie detail page when there are no movies selected. In other
words when the user just clicks on the Movie Detail button on the
left menu.
Now we finish defining all of the classes in our project. Let’s
run it for the last time and see it running in full functionality:
Figure 10-31: Movie Library Project up and running

And this is the end of our Movie Library Project which was
mainly on Object Oriented Programming. I followed an
incremental development approach while building the project. I
tried to make every step crystal clear for you. That’s why we went
step by step. This project is very similar to a real life Python
project. I hope you enjoyed it.
In the next chapter you will have an assignment related to this
project. You will try to re-build the same project by following the
guidelines I prepared for you.
Assignment 2 - Movie Library with Tkinter
We finished our second project in this book which is Project 2 -
Movie Library. Now it’s your turn to recreate the same project.
This chapter is the assignment on the Project 2.
In this assignment you will create a GUI (Graphical User
Interface) application with Python, which is going to be a Movie
Library that replicates the IMDB Top 250 Movies list.
To complete the assignment, follow the instructions where you
see a #TODO statement. You can find the solutions in the previous
chapter.

The Mockups
This will be a big project with lots of modules in it, and at the
end you will have a fully working app, which you can continue to
develop. Here are the mockups of our project:
Figure 10-1: Home Page

In Figure 10-1 you see the Home Page. This is the initial page
for our Movie Library. On the left you see the menu and on the
right there is the content area. In the menu, we have four pages as
Home, Movie List, Movie Detail and About. We will develop the
first three pages in this project. You can continue with more pages
if you want.
Figure 10-2: Movie List

In Figure 10-2 you see the Movie List page. This page contains
the list of IMDB Top 250 movies. It is paginated and we have 10
movies on each page. We have the small image on the first column
of each row. This images are present in the project directory. The
column headers are Row Number (#), Title, Year, Rating and # of
Ratings. You can click on any row to see the details of that movie.
It will navigate you to the Movie Detail page. The color of the
selected button on the left menu becomes orange when we
navigate.
Figure 10-3: Movie Detail

On the Movie Detail Page, we have all the data about the
selected movie: Id, Title, Year, Runtime, Genre, Director, Actors,
Language, Country, Awards, imdbRating, imdbVotes and imdbID.
On the top of the page we have the Title and the big image of the
movie. Big images are provided in the project directory.

We will create a new PyCharm project. The project name is


Movie_Library_ASSIGNMENT. You should download the
project from the Github Repository of the book before starting to
code. Because there are some files that you will need in the project.
We will also install some third party Python packages which are
necessary for the project.
Here is the project that we will develop in this chapter:

Figure 10-4: Movie_Library project for this chapter

As you see in Figure 10-4 we have organized the project in


different directories and packages. We have a main.py file which
will be the entry point for our Movie Library.
Here are the folders and packages which we will create in this
project:
7. data : it will contain data files like colors.py, geometry.py,
imdb_top_250.csv and menus.py
8. images : it will contain folders like home, posters_large and
posters_small for movie images
9. mockup : it will contain the mockup images of the project
10. page : it is a package that contains the page folders as home,
movie_detail and movie_list
11. tkinter_basics : it contains basic widgets to learn Tkinter
12. widget : it is a package that contains the widget folders as
button, left_frame, right_frame and window
Movie Library
Our Movie Library is composed of different modules and
packages. We will have two separate packages as page and
widget . In the page package we will keep our page objects. And
in the widget package we will define the widgets. We will have a
data directory to keep common data objects and files like; colors,
menus, geometry and IMBD Top 250 movies list. Don’t worry, we
will create these folders and packages one by one.

Main Window Structure:


Let’s talk a little bit on the structure of our main window. First
let’s examine the image below:

Figure 10-5: Main Structure of the project

As you see, in our Main Window we have two main sections


as: Left Frame and Right Frame. These are the main containers
for the widgets we will create. We will create classes with these
names, since we want everything to be object oriented in our
project.

The data Folder


Before creating our frame classes, let’s create the data folder
(if you haven’t already). In PyCharm, right click on the project
name and select New > Directory. Name the directory as data
and hit Enter.
Here are the files in this folder: colors.py , geometry.py ,
imdb_top_250.csv and menus.py . We will use these files in
many places in our code, so it is a good idea to keep them in a
central directory where anyone can access. You can find the
imdb_top_250.csv file in the project folder in the Github
Repository of the book.

menus.py :
This file contains a dictionary that stores the menu names and
texts. Here is the content of it:

[1]: 1 # menus.py file


2
3 MENU = {
4 'home': 'Home',
5 'movieList': 'Movie List',
6 'movieDetail': 'Movie Detail',
7 'about': 'About',
8 }

In cell 1, you see definition of the MENU dictionary. The keys


are menu names and the values are the text that we will display on
the screen. We will be needing this dictionary when we create the
menu buttons in the Left Frame.
geometry.py :
This file contains the geometry elements for the main window
as width and height. We define width and height as constants. Here
is the code in this file:

[2]: 1 # geometry.py file


2
3 class GEOMETRY:
4 """Class for Geometry constants."""
5
6 MAIN_WINDOW_WIDTH = 960
7 MAIN_WINDOW_HEIGHT = 880

As you see in cell 2, we define a class named GEOMETRY .


And it has two constants (class attributes) in it. As a convention,
capital letters mean constants in many programming language. So
we define the class name and attribute names in full capitals. The
attributes are MAIN_WINDOW_WIDTH and
MAIN_WINDOW_HEIGHT . Whenever we need to use the main
window width we can just call it as: GEOMETRY.
MAIN_WINDOW_WIDTH .

colors.py :
The last Python file in the data folder is colors.py . In which
we define a simple class called COLORS . This class will store the
constants for the colors we will use throughout the project. Here is
its content:

[3]: 1 # colors.py file


2
3 class COLORS:
4 """
5 Class for Theme colors.
6 """
7
8 # Class Attributes
9 BLACK = '#121212'
10 GRAY = '#686868'
11 ORANGE = 'orange'
12 WHITE = '#ffffff'
13 LIGHT_BLUE = '#5799ef'
14 LIST_ODD_LINE = '#f6f6f5'
15 LIST_EVEN_LINE = '#fbfbfb'

In cell 3, you see the definition of the COLORS class. We


have constants (class attributes) for color codes that we will use for
designing the project theme.

main.py
In most of the Python projects, the entry point is main.py file.
This is where you start the project, create necessary objects and call
related functions to run the project. We will stick to this convention
here, so main.py file is the entry point of our Movie Library.
In our main.py file we will set the structure what you see in
Figure 10-5. We will create the Main Window first, then the Left
Frame and finally the Right Frame. Complete the missing code
in this file:

[4]: 1 # main.py file


2
3 """
4 This is the main file of Movie Library:
5 Pages:
6 1- Home
7 2- Movie List
8 3- Movie Detail
9
10 https://www.imdb.com/chart/top/
11 """
12
from widget import Window, LeftFrame,
13
RightFrame
14
15 if __name__ == '__main__':
16
17 # Root Window
# TODO - Create the root object by calling
18
Window class
19
20 # LEFT FRAME
# TODO - Create the left frame by calling
21
LeftFrame class
22
23 # RIGHT FRAME
# TODO - Create the right frame by calling
24
RightFrame class
25
26 # start root window -> mainloop()
27 # TODO - start the main window (root)

In cell 4, you see the code in our main.py file.


It starts by importing the classes for the main window and
frames. As you see in the structure in Figure 10-5, there are three
parts in the main.py file which are:
4. Root Window which is the main window and the class for it is:
Window
5. Left Frame is left side of the screen and the class is:
LeftFrame
6. Right Frame is right hand side and the class is: RightFrame
And finally we will have a method to call the mainloop() to
start our application and keep it running.

Packages
We want our Movie Library project to be modular as much as
possible. This is the key for scalability. If you have a good
organization of your files and folders, then no matter the size of
your project, you can still manage it without trouble. When your
project size becomes too big, a proper organization may save your
life. Or it may be just the opposite with a bad organization.
We will define two packages in our project which are: widget
and page . Let’s create them first:

widget Package:
We will create a Python package for our widgets. We want all
of our widgets to be located in the same directory, which will make
things easier when anyone needs to use them. Each widget will be
in a separate folder. And in each folder there will be a class for that
widget. Here are the folders for widgets: button , left_frame ,
right_frame and window .
To create the widget package, in PyCharm right click on the
project name and select New > Python Package. Name the
package as widget and hit Enter. Now you have a package named
widget that has the __init__.py file in it. Remember that, this
__init__.py file is the entry point for packages. We will use it in a
minute.

page Package:
This will be the package for the pages in our project. Each page
will be in a separate folder, in which we will have a class for that
page. The folders for pages are: home , movie_detail , and
movie_list .
To create the page package, in PyCharm right click on the
project name and select New > Python Package. Name the
package as page and hit Enter. Now you have a package named
page that has the __init__.py file in it.
Please see the image below for the packages and folders in
them:

Figure 10-6: Packages

Now that we have our packages created, let’s define the related
classes. We will start by the definition of the Window class.
Window
The Window class will work as a wrapper for the main
window and related functionalities. It will be located in a folder
named window in the widget package (see Figure 10-6). So you
have to create the window folder first, then the class in it. The file
name for the class Window.py which is the same as the class
name it contains. So the full path for the class is:
widget/window/Window.py . Complete the code of the Window
class:

[5]: 1 # widget/window/Window.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.geometry import GEOMETRI
6
7 class Window:
8 """
9 It creates tkinter main window.
10 """
11
12 def __init__(self, title):
13 # TODO - create the main window object
14 self.window.title(title)
15 self.window.configure(bg=COLORS.BLACK)
16 self.set_size()
17
18 # tkinter start
19 def start_method(self):
20 self.window.mainloop()
21
22 # method for window width & height
23 def set_size(self):
w, h = GEOMETRI .MAIN_WINDOW_WIDTH,
24
GEOMETRI.MAIN_WINDOW_HEIGHT
25 self.window.geometry('%dx%d' % (w, h))

Now we finished the definition of the Window class, but it’s


not enough to be able to use it. Since it is in a package called
widget , we have to import it in the __init__.py file of this
package. Let’s do it:

[6]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window

In cell 6, you see the code in thee __init__.py file of widget


package. This file is the entry point of this package, so it is
responsible for doing all the imports for the classes and objects that
belong to the package.
In line 9, we import the Window class as: from
.window.Window import Window. Please be careful that the file
path starts with a dot (.) and this is not a mistake. The dot (.)
means the current package. So here is how we read the statement
below.
.window.Window : in the current package we have a folder
called window and in that folder we have a file called Window .
The general syntax for importing classes in the __init__.py
files is: from .[file_path] import Class. We will use this syntax
very frequently in our project. Now we are ready to use the
Window class in any file in the project.

Left Frame
Let’s start to build our left frame. It is going to be the container
for the left menu buttons.
To create the LeftFrame class we will create a folder and
Python file first. In the widget package, create a directory named
left_frame . And in the left_frame folder create a Python file
named LeftFrame.py . This is where we define our LeftFrame
class. Complete the code of the LeftFrame class:

[7]: 1 # widget/left_frame/LeftFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from data.menus import MENU
6
7 # exact import
8 from widget.button.Button import Button
from widget.right_frame.RightFrame import
9
RightFrame
10
11 class LeftFrame:
12 """
13 Class for Left Frame to hold the menu items.
14 """
15
# TODO - define the init method for the
16 LeftFrame
17
18 def add_frame(self):
self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=
19
(62, 0))
20
# method for click event of menu buttons -> event
21
handler
22 def handle_click(self, event):
23 self.manage_button_colors(event)
24 page_name = str(event.widget).split('.')[2]
# print('Page {0} is
25
clicked.'.format(page_name))
26
27 # self is left frame -> master = root
28 # right frame is in children of master
29 rightFrame = self.master.children['rightFrame']
30
31 # destroy children
# TODO - call destroy_children() method of
32
RightFrame
33
34 # add new page -> page_name
# TODO - call frame_content() method of
35
RightFrame
36
37 def add_menus(self):
38 # add menus in loop
39 for menu_key, menu_text in MENU .items():
40 if menu_key == 'about':
button = # TODO - create the Button
41
Widget
42 else:
43 # TODO - create the Button Widget
44
45 if menu_key == 'home':
46 self.selected_button_color(button.button)
47
48 def manage_button_colors(self, event):
49 # clicked button -> event.widget
# all the menu buttons ->
50
event.widget.master.children
for child in
51
event.widget.master.winfo_children():
52 if child == event .widget:
child.configure(bg=COLORS.ORANGE,
53
fg=COLORS.WHITE)
54 else:
# TODO - configure the child as
55
background = BLACK and foreground = ORANGE
56
57 def selected_button_color(self, button):
58 # tk button configure
# TODO - configure the button as background =
59
ORANGE and foreground = WHITE

We finished the definition of the LeftFrame . To display it, we


have to call it from the main.py file. But to be able to call it from
main.py file, we have to import it in the __init__.py file of the
widget package. Let’s do it:

[8]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame

In cell 8, we import the LeftFrame class in the __init__.py


file of the widget package as: from .left_frame.LeftFrame
import LeftFrame.

Button Class
Under the widget package create a new folder called button .
In this folder create a new Python file as Button.py . We will
define our Button class in this file. Be careful that, the file name
and the class name are the same, as we did before.
This class will be a wrapper class for the Tkinter Button . Why
do we need a wrapper class? Because, we can add new
functionalities to existing ones that comes from Tkinter and that
will become our own implementation in our project. The wrapper
will give the complete control over the Button objects that will be
created in the project. Complete the code of the Button class:

[9]: 1 # widget/button/Button.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 # Wrapper Class -> wrapped the tkinter.Button()
7 class Button:
8 """It will create Tkinter Button."""
9 def __init__(self, master, name, text,
10 fg, bg, width, height,
11 handle_click,
12 padx=0, pady=0, side=tk.TOP):
13 self.button = # TODO - create tkinter Button
14 self.padx = padx
15 self.pady = pady
16 self.side = side
17 self.add_button()
18 self.bind_event(handle_click)
19
20 def add_button(self):
21 self.button.configure(font=('Arial', 12))
22 self.button.pack(
23 padx = self.padx,
24 pady = self.pady,
25 side = self.side
26 )
27
28 # event binding to button
29 def bind_event(self, handle_click):
# in tkinter -> .bind() -> left mouse click ->
30
<Button-1>
31 self.button.bind('<Button-1>', handle_click)

We finished the definition of our Button class. Let’s add it to


the __init__.py file of the widget package:

[10]: 1 # __init__.py file of the widget package


2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame
11 from .button.Button import Button

In cell 10, we import the Button class in the __init__.py file


of the widget package as: from .button.Button import Button.
Right Frame
Now it’s time to create the right frame. We will define a
RightFrame class for this purpose. It’s going to keep the pages on
the right hand side of the screen. Actually these pages will be the
content of our application. Remember that we had the MENU
dictionary for the pages. Here are our pages: Home, Movie List,
Movie Detail, and About. We will create the first three in this
project and I will leave the About page to you. And the
RightFrame class will be the container for these pages.
To create the RightFrame class we will create a folder and
Python file first. In the widget package, create a directory named
right_frame . And in the right_frame folder create a Python file
named RightFrame.py . This is where we define our RightFrame
class. Complete the code of the right frame:

[11]: 1 # widget/right_frame/RightFrame.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5
6 from page import Home, MovieList, MovieDetail
7
8 class RightFrame:
9 """It will hold the pages on the right frame."""
10
11 # class attribute
12 bg_color = COLORS .ORANGE
13
14 # TODO - define the init method
15
16 def add_frame(self):
17 # frame content
18 self.frame_content()
self.frame.pack(side=self.side, fill=tk.BOTH,
19
expand=True)
20
21 def frame_content(self, page_name='home'):
22 try:
23 incoming_frame = self.frame
24 except:
25 incoming_frame = self
26 finally:
27 if page_name == 'home':
28 # add home page
Home(incoming_frame,
29
RightFrame.bg_color)
30 # TODO - create other pages
31
# static method -> Class Method (no self
32
parameter)
33 def destroy_children(frame):
34 # destroy the children
35 for child in frame .winfo_children():
36 child.destroy()

To be able to display the RightFrame we have to import it in


the __init__.py file of the widget package. Then we have to call
in in the main.py file. Let’s complete these steps:
[12]: 1 # __init__.py file of the widget package
2
3 """
4 modules and packages -> main entry point
5 """
6
7 # import modules and packages -> . notation
8 # from .[file_path] import Class
9 from .window.Window import Window
10 from .left_frame.LeftFrame import LeftFrame
11 from .button.Button import Button
from .right_frame.RightFrame import
12
RightFrame

In cell 12, in the __init__.py file of the widget package, we


import the RightFrame class as: from
.right_frame.RightFrame import RightFrame.

Home Page
Let’s create the home page of our application. We will create
the all of the pages under the page package. For the home page,
the class name will be in the same name as Home . Let’s start by
creating a new folder named home in the page package. And in
this home directory create a new Python file as Home.py .
Remember by convention, in our project the file names and the
classes are usually the same.
Complete the missing code in the Home class:

[13]: 1 # page/home/Home.py file


2
3 import tkinter as tk
4 from PIL import Image, ImageTk
5 from data.colors import COLORS
6
7 class Home:
8 """It creates the Home Page."""
9
10 # TODO - define the init method
11
12 def add_frame(self):
self.frame.pack(side=self.side, fill=tk.BOTH,
13
expand=True)
14
15 def frame_content(self):
16 course = tk .Label(self.frame,
17 text='Python Hands-On',
18 font=('Helvetica', 18, 'bold'),
19 fg=COLORS.BLACK,
20 bg=self.bg_color)
21 course.place(x=315, y=50)
22
23 instructor = tk .Label(self.frame,
24 text='(Musa Arda)',
25 font=('Helvetica', 18, 'bold'),
26 fg=COLORS.BLACK,
27 bg=self.bg_color)
28 # TODO - place instructor at x=344, y=105
29
30 # TODO - render the image
31
32 project = tk .Label(self.frame,
text='Capstone Project - Movie
33
Library with Tkinter',
34 font=('Helvetica', 18, 'bold'),
35 fg=COLORS.WHITE,
36 bg=COLORS.BLACK)
37 project.place(x=140, y=765)
38
39 def render_image(self):
load =
40
Image.open('images/home/python_logo.png')
41 render = ImageTk .PhotoImage(load)
img_lb = tk .Label(self.frame, image=render,
42
bg=self.bg_color)
43 img_lb.image = render
44 img_lb.place(x=136, y=180)

Now we should import the Home class in the __init__.py file


of the page package. Below is the code for that:

[14]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail
6 """
7
8 from .home.Home import Home

Pillow Package:
For rendering the Python logo, which is an image, we need to
install a third-party package that is the Pillow package. Pillow is a
not built-in package in Python. So we have to install it in the virtual
environment of our current project. We will install it in PyCharm.
If you need help on how to install packages in PyCharm please
revisit the Installing Packages section in Chapter 4 – Modules and
Packages. Where you will see the necessary steps in detail. Below
you can see an image for the package you should install. Just click
on Install Package after you find it.

Figure 10-7: Installing the Pillow package in PyCharm

Movie List
In this sub-section we will create the MovieList class which
will be our movie list page. This is the second page in our
application and has a table like structure in it. The data for the
movies comes from the imdb_top_250.csv file which is in the
data folder. This data was acquired by doing web scraping with
Python on the IMDB Top 250 Movies list page.
Let’s start by creating a new folder named movie_list in the
page package. And in this movie_list directory create a new
Python file as MovieList.py . The file name and the class name are
again the same.
Complete the missing code lines in the MovieList class:

[15]: 1 # page/movie_list/MovieList.py file


2
3 import tkinter as tk
4 from tkinter import ttk
5 import csv
6 from PIL import Image, ImageTk
7
8 from data.colors import COLORS
from page.movie_detail.MovieDetail import
9
MovieDetail
10
11 class MovieList:
12 """This class is the frame for movie list."""
13
14 # pagination variables
15 page_number = 1
16 movies_per_page = 10
17 total_num_pages = 0
18
19 # columns list
columns = [ 'imdbID', 'Id', 'Title', 'Year',
20
'imdbRating', 'imdbVotes']
21
22 # TODO - define the init method
23
24 def add_frame(self):
25 self.add_page_title('Movie List')
26 self.read_csv()
27 self.create_page()
self.frame.pack(side=self.side, fill=tk.BOTH,
28
expand=True)
29
30 def add_page_title(self, title):
31 lbl = tk .Label(self.frame, text=title, height=3,
bg=COLORS.BLACK,
32
fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
33
padx=1, pady=(0, 8), sticky='we')
34
35 def read_csv(self):
36 movie_path = 'data/imdb_top_250.csv'
37
38 # TODO - read the csv file and fill self.movies
39
40 # assign total_num_pages
MovieList.total_num_pages = len(self.movies)
41
// MovieList .movies_per_page + 1
42
43 def create_page(self):
44 self.add_header_row()
45 self.create_table()
46 self.create_combo_box()
47
48 def add_header_row(self):
for j, column in
49
enumerate(MovieList.columns):
50 if column != 'imdbID':
lbl = tk .Label(self.frame,
51 text=str(column), width=54, height=2,
bg=COLORS.BLACK, fg=COLORS.WHITE,
52 font=('Arial', 10, 'bold'))
53 # configure width
54 if column == 'Id':
55 lbl.configure(text='#', width=4)
56 elif column == 'Year':
57 lbl.configure(width=8)
58 elif column == 'imdbRating':
59 lbl.configure(text='Rating', width=8)
60 elif column == 'imdbVotes':
lbl.configure(text='# of Ratings',
61
width=12)
62
63 # place in a grid
64 if column == 'imdbVotes':
lbl.grid(row=1, column=j, sticky='we',
65
padx=(0, 10))
66 else:
lbl.grid(row=1, column=j, sticky='we',
67
padx=(0, 1))
68
69 def create_table(self):
70 for i, movie in enumerate(self.movies):
71 # if i = 2
72 # 10 <= i < 20
if (MovieList .page_number - 1) *
MovieList.movies_per_page <= i <
73
MovieList.page_number *
MovieList.movies_per_page:
for j, key in
74
enumerate(MovieList.columns):
name = 'table_row_' + str(i) + str(j) + '_'
75
+ movie[ 'imdbID']

76 if j == 0:
77 # render image
78 # TODO - render the image
79 else:
80 # print label
81 # TODO - write the labels
82
83 self.i = i + 3
84
85 def render_image(self, movie, i, j, name):
86 try:
87 # load the image
load = Image .open('images/posters_small/' +
88
movie['imdbID'] + '.jpg')
89 except:
90 # load no image
load =
91
Image.open('images/posters_small/no_image.jpg')
92 finally:
93 render = ImageTk .PhotoImage(load)
lbl_img = tk .Label(self.frame, name=name,
94
image=render, bg=COLORS.ORANGE)
95 lbl_img.image = render
lbl_img.grid(row=i+2, column=j, padx=(7,0),
96
sticky='we')
97
98 def write_label(self, movie, i, j, key, name):
lbl = tk .Label(self.frame, name=name,
99 text=str(movie[key]), height=4,
bg=COLORS.WHITE, fg=COLORS.BLACK,
font=('Arial', 10, 'bold'),
100
cursor='hand2')
101
102 # bind the left click event
103 lbl.bind('<Button-1>', self.movie_click)
104
105 # configure width
106 # TODO - modify the width of columns
107
# TODO - modify background color for odd
108
and even lines
109
110 # place in a grid
111 if key == 'imdbVotes':
lbl.grid(row=i+2, column=j, sticky='we',
112
padx=(0, 10))
113 else:
lbl.grid(row=i+2, column=j, sticky='we',
114
padx=(0, 1))
115
116 def fill_bg(self, widget, i):
117 # check if the row (i) is odd
118 if i % 2 == 1:
119
widget.configure(bg=COLORS.LIST_ODD_LINE)
120 else:
121
widget.configure(bg=COLORS.LIST_EVEN_LINE)
122
123 def create_combo_box_select_event(self, event):
MovieList.page_number =
124
int(event.widget.get())
125 # clear the table cells
126 self.clear_table(event)
127 # recreate the table
128 self.create_table()
129
130 def create_combo_box(self):
131 # each page -> 10 movies
132 # total 250 movies
133 # to upper limit 250 / 10 = 25
values = list(range(1,
134
MovieList.total_num_pages))
pages = ttk .Combobox(self.frame,
135
values=values, width=4)
136 # index of the page (1 minus page_number)
137 pages.current(MovieList.page_number - 1)
138
139 # bind select event
pages.bind('<<ComboboxSelected>>',
140
self.create_combo_box_select_event)
141 pages.grid(row=self.i, column=2, pady=(15,0))
142
143 def clear_table(self, event):
144 master = event .widget.master
145 # loop over the children
146 master_children_copy = master .children.copy()
147 for child in master_children_copy:
148 if 'table_row_' in child:
149 master.children[child].destroy()
150
151 def movie_click(self, event):
152 imdbID = str(event.widget).split('_')[3]
153
# modify the right frame (clear the right
154
frame)
155 # TODO - modify Right Frame
156
157 # modify the left frame
158 # TODO - modify Left Frame
159
160 def modify_right_frame(self, event, imdbID):

161 rightFrame = event .widget.master


162 # loop over the children of rightframe
163 for child in rightFrame .winfo_children():
164 child.destroy()
165
166 # add Movie Detail
167 # TODO - call MovieDetail page
168
169 def modify_left_frame(self, event):
170 # get the root element
171 root = event .widget.master.master.master
172 for child in root .winfo_children():
173 if str(child) == '.leftFrame':
174 for ch in child .winfo_children():
175 if str(ch) == '.leftFrame.movieDetail':
ch.configure(bg=COLORS.ORANGE,
176
fg=COLORS.WHITE)
177 else:
ch.configure(bg=COLORS.BLACK,
178
fg=COLORS.ORANGE)

As the last step, let’s add our MovieList to the __init__.py


file of the page package:

[16]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail

6 """
7
8 from .home.Home import Home
9 from .movie_list.MovieList import MovieList

Movie Detail
Now it’s time to develop the Movie Detail page. When the user
clicks on a movie in the Movie List, we want to display the details
of that movie in a new page. This new page will be the
MovieDetail class and the user will be navigated to that page on
click.
Let’s start by creating a new folder named movie_detail in the
page package. And in this movie_detail directory create a new
Python file as MovieDetail.py . The file name and the class name
are again the same.
Complete the missing lines in the MovieDetail class:

[17]: 1 # page/movie_detail/MovieDetail.py file


2
3 import tkinter as tk
4 from data.colors import COLORS
5 from PIL import Image, ImageTk
6
7 class MovieDetail:
8 """Page to hold movie detail."""
9
10 # TODO - define the init method
11
12 def add_frame(self):
13 self.add_content()
14 self.add_page_title('Movie Detail')
self.frame.pack(side=self.side, fill=tk.BOTH,
15 expand=True)
16
17 def add_page_title(self, title):
18 if self.imdbID != None:
lbl = tk .Label(self.frame,
19
text=self.movie['Title'], height=3,
bg=COLORS.BLACK,
20
fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
lbl.grid(row=0, column=0, columnspan=8,
21
padx=1, pady=(0, 8), sticky='we')
22 else:
lbl = tk .Label(self.frame, text=title,
23
height=3,
bg=COLORS.BLACK,
24
fg=COLORS.WHITE, font=('Arial', 12, 'bold'))
25 lbl.pack(fill=tk.BOTH, padx=(1, 0))
26
27
28 def add_content(self):
29 if self.imdbID != None:
30 self.render_image()
31 self.get_movie()
32 self.render_keys()
33 self.render_values()
34 else:
35 pass
36
37 def render_image(self):
38 # TODO - render the large image
39
40 def get_movie(self):
# TODO - assign the incoming movie to
41
self.movie (you will look in self.movies list)

42
43 def render_keys(self):
44 for i, key in enumerate(self.movie.keys()):
45 txt = str(key)
lbl = tk .Label(self.frame, text=txt, height=2,
46
width=12, anchor='w')
47 self.fill_bg(lbl, i)
48 lbl.grid(row=i+2, column=0, padx=(10, 1))
49
50 def fill_bg(self, widget, i):
51 # check if the row (i) is odd
52 if i % 2 == 1:
53
widget.configure(bg=COLORS.LIST_ODD_LINE)
54 else:
55
widget.configure(bg=COLORS.LIST_EVEN_LINE)
56
57 def render_values(self):
58 # TODO - render the values

Let’s add the MovieDetail class to the __init__.py file of the


page package:

[18]: 1 # __init__.py file of page package


2
3 """
4 package for pages.
5 Pages: Home, Movie List, Movie Detail
6 """
7
8 from .home.Home import Home

9 from .movie_list.MovieList import MovieList


from .movie_detail.MovieDetail import
10
MovieDetail

And this is the end of our assignment on Movie Library. For the
solutions you can check Chapter 10 Project 2 - Movie Library.
12. Final Exam
The Final Exam includes the topics we have covered in this
book. It is a multiple choice exam with 20 questions. Each
question is 5 points, which makes 100 points in total. You
should get at least 70 points to be successful. The duration is
120 minutes.

Here is the number of questions in the respective chapters:

Topic Questions
Exception
3
Handling
Modules &
3
Packages
Format Operations 2
File Operations 3
OOP 7
Tkinter 2
Total 20

If you want to solve the final exam in Jupyter environment, you


can download the notebook file for the final exam,
Final_Exam.ipynb, from the Github Repository of this book. It
is in the folder named 12_Final_Exam.

Here are the questions for the Final Exam:


QUESTIONS
Q1:

The "total_likes" function below is going to sum up all the


likes.
The likes are in a list of dictionaries called "posts".
Each post is a dictionary.
But there is a problem in the "total_likes" function.
And you are expected to fix this problem by using try-except-
else structure.
Which one below is the correct definition of this function?
(You don't want the function to throw any exceptions.)

def total_likes():
posts = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

for post in posts:


total = total + post[ 'Like']

return total

sum_of_all_likes = total_likes()
print(sum_of_all_likes)

A def total_likes():
posts = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

try:
for post in posts:
total = total + post[ 'Like']
except:
raise Exception('Error in summation.')

return total

sum_of_all_likes = total_likes()
print(sum_of_all_likes)

B def total_likes():
posts = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

try:
for post in posts:
total = total + post[ 'Like']
except:
pass

return total

sum_of_all_likes = total_likes()
print(sum_of_all_likes)

C def total_likes():
posts = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

for post in posts:


try:
post_like = post[ 'Like']
except:
continue
else:
total = total + post_like

return total

sum_of_all_likes = total_likes()
print(sum_of_all_likes)

D def total_likes():
posts = [{ 'Image': 4, 'Like': 20, 'Comment': 12},
{'Like': 15, 'Comment': 8, 'Share': 10},
{'Image': 7, 'Comment': 16, 'Share': 37},
{'Image': 6, 'Like': 10, 'Comment': 9}]

total = 0

for post in posts:


try:
post_like = post[ 'Like']
except:
break
else:
total = total + post_like

return total

sum_of_all_likes = total_likes()
print(sum_of_all_likes)

Q2:

You want to define a function that takes a list as parameter.


The function will concatenate all the letters in the list and it
will create a single word.
If the list item is not a letter (alphabetical) it will not
concatenate it.
But, for that item it will print as follows:
"item 'x' is not a letter"
Which one below is the correct definition of this function?
(You don't want the function to throw any exceptions.)

Example Function Call:


secret_word_in_list(['p', 1, 'a', '!', 'y', 'b', 3, '-', '*', 'on'])

Expected Output:
item 1 is not a letter
item ! is not a letter
item 3 is not a letter
item - is not a letter
item * is not a letter
paybon

A def secret_word_in_list(a_list):
word = ""
for i in a_list:
try:
assert int(i)
except:
print("item {0} is not a letter".format(i))
pass
else:
word += i
return word

B def secret_word_in_list(a_list):
word = ""
for i in a_list:
try:
assert i .isalpha()
except:
print("item {0} is not a letter".format(i))
pass
else:
word += i
return word

C def secret_word_in_list(a_list):
word = ""
for i in a_list:
try:
assert i .isalpha()
else:
word += i
return word

D def secret_word_in_list(a_list):
word = ""
for i in a_list:
try:
assert i .isalpha()
except:
raise "item {0} is not a letter".format(i)
else:
word += i
return word

Q3:

You want to define a function that reads a file.


It will take the file path as the parameter.
If the file does not exist, it will not raise an exception.
Instead it will catch FileNotFoundError exception and print as
follows:
"No such file..."
If the file exists, it will print the file content.
In any case (finally) it will close the file.
Which one below is the correct definition of this function?

A def read_file(path):
try:
file = open(path)
except FileNotFoundError as err:
raise err
else:
print(file.read())
finally:
file.close()

B def read_file(path):
try:
file = open(path)
except:
file.close()
else:
print(file.read())
finally:
raise FileNotFoundError

C def read_file(path):
try:
file = open(path)
except FileNotFoundError:
raise
else:
print(file.read())
finally:
try:
file.close()
except:
pass

D def read_file(path):
try:
file = open(path)
except FileNotFoundError:
print("No such file...")
else:
print(file.read())
finally:
try:
file.close()
except:
pass
Q4:

Which one below will give you the current folder that Python
runs in?

A import os
project_path = os .getcwd()

B import sys
project_path = sys .path

C import os
project_path = os .environ

D import sys
project_path = sys .gettrace()

Q5:

Let's say you want to import the Path class from pathlib module.
Which one below is not appropriate for this task?

A import pathlib
path = pathlib .Path('.')

B import pathlib as Path


path = Path( '.')

C from pathlib import Path


path = Path( '.')

D from pathlib import *


path = Path( '.')
Q6:

Which one below is correct about Python modules and


packages?

In Python, a package is an ordinary Python file ending with


A
'.pck' extension.

B We use 'include' keyword to import Python packages.

C In Python, packages are containers that hold modules.

In Python, a package is just an ordinary folder. To create a


D
package it is enough to create a folder.

Q7:

We have a dictionary as below:

capitals = {
'UK': 'London',
'Germany': 'Berlin',
'Canada': 'Ottawa',
'US': 'Washington'
}

Which one below can print the following text by using the
capitals dictionary?
"Capital of Germany is Berlin."

A country = 'Germany'
s = "Capital of {0} is {1}.".format(country,
capitals[country])
print(s)

B country = 'Germany'
s = "Capital of {1} is {0}.".format(country,
capitals[country])
print(s)

C country = 'Germany'
s = "Capital of {} is {}.".format(capitals[country], country)
print(s)

D country = 'Germany'
s = "Capital of {0} is {1}.".format(capitals[country],
country)
print(s)

Q8:

We have a dictionary as below:

capitals = {
'UK': 'London',
'Germany': 'Berlin',
'Canada': 'Ottawa',
'US': 'Washington'
}

Which one below can print the following text for all the items in
the capitals dictionary?
Capital of [Country] is [City].

Expected Output:
Capital of UK is London.
Capital of Germany is Berlin.
Capital of Canada is Ottawa.
Capital of US is Washington.

[print(f"Capital of {co} is {ci}.") for co, ci in


A
capitals.values()]

B [print(f"Capital of {a} is {b}.") for b, a in capitals .items()]

C [print(f"Capital of {s} is {u}.") for s, u in capitals .keys()]

[print(f"Capital of {a} is {b}.") for a, b in


D
capitals.items()]

Q9:

We want to get the names of all the folders and files in the
current directory.
Which one below cannot give this?

A import os
from pathlib import Path

for content in os .listdir():


print(content)

B import os

for content in os .scandir('.'):


print(content.name)

C import os
from pathlib import Path

for content in Path( '.'):


print(content.name)

D import os
from pathlib import Path

for content in Path() .iterdir():


print(content.name)

Q10:

We want to create the folder tree below in the current folder.

* K1
* K1_1
* K1_2
* K2
* K3
* K3_1

Which one below can achieve this?

A import os

os.mkdirs('K1/K1_1')
os.mkdirs('K1/K1_2')
os.mkdirs('K2')
os.mkdirs('K3/K3_1')

B import os

os.makedirs('K1/K1_1')
os.makedirs('K1/K1_2')
os.makedirs('K2')
os.makedirs('K3/K3_1')
C import os

os.makedirs('K1/K1_1')
os.makedirs('K1/K1_2')
os.makedir('K2')
os.makedirs('K3/K3_1')

D import os

os.mkdirs('K1/K1_1')
os.mkdirs('K1/K1_2')
os.mkdir('K2')
os.mkdirs('K3/K3_1')

Q11:

You will search for files in a directory.


You want to use a pattern to find all Python files (with .py
extension) that includes letter 'k' in their names.
You will only list the files, not the folders.
Which one below can achieve this task?

A import os
import fnmatch

path = r'<folder path>'


pattern = 'k*.py'

[print(file.name)
for file in os .scandir(path)
if file.is_file() and fnmatch .fnmatch(file.name, pattern)]

B import os
import fnmatch

path = r'<folder path>'


pattern = '*k.py'

[print(file.name)
for file in os .scandir(path)
if fnmatch .fnmatch(file.name, pattern)]

C import os
import fnmatch

path = r'<folder path>'


pattern = '*k*.py'

[print(file.name)
for file in os .scandir(path)
if not file.is_file() and fnmatch .fnmatch(file.name,
pattern)]

D import os
import fnmatch

path = r'<folder path>'


pattern = '*k*.py'

[print(file.name)
for file in os .scandir(path)
if file.is_file() and fnmatch .fnmatch(file.name, pattern)]

Q12:

You want to define a class named SuperHero.


SuperHero has 3 attributes:
name (str)
real_name (str)
age (int)
You will create two super heroes: batman and superman.
Then you will print the real names of these heroes.
Here are their data:
name: Batman, real_name: Bruce Wayne, age: 30
name: Superman, real_name: Clark Kent, age: 300
Which one below is the correct way of doing these?

Expected Output:
Bruce Wayne
Clark Kent

A class SuperHero:
def __init__(self, age, real_name, name):
self.name = name
self.real_name = real_name
self.age = age

batman = SuperHero( 30, 'Bruce Wayne', 'Batman')


superman = SuperHero( 300, 'Clark Kent', 'Superman')

print(batman.real_name)
print(superman.real_name)

B class SuperHero:
def __init__(self, name, real_name, age):
self.name = name
self.real_name = real_name
self.age = age

batman = SuperHero( 'Bruce Wayne', 'Batman', 30)


superman = SuperHero( 'Clark Kent', 'Superman', 300)

print(batman.real_name)
print(superman.real_name)

C class SuperHero():
self.name = name
self.real_name = real_name
self.age = age

batman = SuperHero( 'Bruce Wayne', 'Batman', 30)


superman = SuperHero( 'Clark Kent', 'Superman', 300)

print(batman.real_name)
print(superman.real_name)

D class SuperHero(name, real_name, age):


def __init__(self):
self.name = name
self.real_name = real_name
self.age = age

batman = SuperHero( 'Bruce Wayne', 'Batman', 30)


superman = SuperHero( 'Clark Kent', 'Superman', 300)

print(batman.real_name)
print(superman.real_name)

Q13:

You have the Vehicle class as below:

class Vehicle:
def __init__(self, brand, km, age):
self.brand = brand
self.km = km
self.age = age

# the age of vehicle increases by 1


def set_age(self):
self.age = self.age + 1

def go(self, km):


self.km += km

You will define a child class which inherits from the Vehicle
class.
Its name will be Car.
Car has an extra attribute as 'model'.
And the age of Car increases by 10 instead of 1. (set_age)
Which one below is the correct definition of the Car class?

A class Car(Vehicle):
def __init__(self, brand, km, age, model):
super().__init__()
self.model = model

def set_age(self):
self.age += 10

B class Car(Vehicle):
def __init__(self, brand, km, age, model):
super().__init__(brand, km, age)
self.model = model

def set_age(self):
self.age += 10
C class Car(Vehicle):
def __init__(self, brand, km, age, model):
super().__init__(brand, km, age, model)

def set_age(self):
self.age += 10

D class Car(Vehicle):
def __init__(self, brand, km, age, model):
super().__init__(brand, km, age)
self.model = model
self.age += 10

Q14:

You want to define a class named Circle.


It has an attribute as r (radius).
And it has two methods, to return the area and the perimeter of
the circle.
Which one below is the correct definition of the Circle class?

Example:
circle = Circle(8)
print(circle.area())
print(circle.perimeter())

Expected Output:
201.06192982974676
50.26548245743669

A from math import pi

class Circle:
def __init__(radius):
self.r = radius

def area(self):
return pi * self.r**2

def perimeter(self):
return 2 * pi * self.r

B from math import pi

class Circle(object):
def __init__(self, r):
self.r = r

def area(self):
return pi * r **2

def perimeter(self):
return 2 * pi * r

C from math import pi

class Circle(object):
def __init__(self, radius):
self.r = radius

def area(self):
return pi * self.r**2

def perimeter(self):
return 2 * pi * self.r
D from math import pi

class Circle:
def __init__(self, r):
self.r = r

def area(self):
area = pi * self.r**2

def perimeter(self):
perimeter = 2 * pi * self.r

Q15:

You want to define a class named Printer.


It has two attributes: 'printer_name' and 'model'.
We want to call the print() method on Printer class as below:

Call the class:


printer = Printer('Laser', 'Xerox')
print(printer)

Expected Output:
Laser says -> ...zzz...printing...zzz...
Which one below is the correct definition of the Printer class?

A class Printer(object):
def __init__(self, pname, pmodel):
self.printer_name = pname
self.model = pmodel

def __print__(self):
return f '{self.printer_name} says ->
...zzz...printing...zzz...'
B class Printer(object):
def __init__(self, pname, pmodel):
self.printer_name = pname
self.model = pmodel

def __add__(self):
return f '{self.printer_name} says ->
...zzz...printing...zzz...'

C class Printer(object):
def __init__(self, pname, pmodel):
self.printer_name = pname
self.model = pmodel

def __str__(self):
return '{1} says ->
...zzz...printing...zzz...'.format(self.printer_name)

D class Printer(object):
def __init__(self, pname, pmodel):
self.printer_name = pname
self.model = pmodel

def __str__(self):
return f '{self.printer_name} says ->
...zzz...printing...zzz...'

Q16:

What is the result of the print() function below?

class X:
def x(self):
return self.y()

def y(self):
return 'Y'

class Y(X):
def y(self):
return 'Z'

x = X()
y = Y()
print(x.x(), y.y())

A YZ

B XY

C X Z

D YX

Q17:

Which one below is not correct about OOP (Object Oriented


Programming)?

Class is the blueprint that defines how the object should be


A
created.

If we do not want anyone to access the attributes in the


B class, we define these attributes as private. This behavior is
called Encapsulation.

When a class inherits from a parent class, it cannot modify


C the attributes and the methods of the parent class.

D A class can inherit from multiple classes.

Q18:

We have the class definitions and X.mro() call as follows:

class A:
pass

class B:
pass

class C:
pass

class D(A):
pass

class X(B, D):


pass

[m.__name__ for m in X .mro()]

Which one below is the correct ordering of MRO (Method


Resolution Order) of class X?

A ['B', 'D', 'A', 'X', 'object']

B ['X', 'B', 'D', 'A', 'object']

C ['X', 'A', 'B', 'D', 'object']


D ['A', 'B', 'D', 'X', 'object']

Q19:

You want to create a GUI (Graphical User Interface) application


by using tkinter package of Python.
The app will render a Combobox on the screen.
Combobox will have 4 values: 'Python', 'JavaScript', 'Java', 'C#'
Which one below is the correct definition of this app?

A import tkinter as tk
from tkinter import ttk

sting_var = tk .StringVar()
values = [ 'Python', 'JavaScript', 'Java', 'C#']

cmb = ttk .Combobox(textvariable=sting_var,


values=values)

cmb.pack(padx=20, pady=20)

cmb.mainloop()

B import tkinter as tk
from tkinter import ttk

root = tk .Tk()
sting_var = tk .StringVar()
values = [ 'Python', 'JavaScript', 'Java', 'C#']

cmb = ttk .Combobox(root, textvariable=sting_var,


values=values)
root.pack(padx=20, pady=20)

root.mainloop()

C import tkinter as tk
from tkinter import ttk

root = tk .Tk()
sting_var = tk .StringVar()
values = [ 'Python', 'JavaScript', 'Java', 'C#']

cmb = ttk .Combobox(root, textvariable=sting_var,


values=values)

cmb.pack(padx=20, pady=20)

root.mainloop()

D import tkinter as tk
from tkinter import ttk

root = tk .Tk()
sting_var = tk .StringVar()
values = [ 'Python', 'JavaScript', 'Java', 'C#']

ttk.Combobox(cmb, textvariable=sting_var, values=values)

cmb.pack(padx=20, pady=20)

root.mainloop()

Q20:
You want to create a GUI (Graphical User Interface) application
by using tkinter package of Python.
It will render two buttons on the screen.
The texts on the buttons will be: 'Hello' and 'Exit'
When the user clicks the 'Hello' button it will print as;
"Tkinter GUI is fun..."
When the user clicks the 'Exit' button it will close the main
window and exit the program
Which one below is the correct definition of this app?

A import tkinter as tk

def greet():
print("Tkinter GUI is fun...")

def exit():
btn_exit.destroy()

root = tk .Tk()
frame = tk .Frame(root)
frame.pack()

btn_greet = tk .Button(frame, text="Hello", bg='green',


fg='white', command=greet)
btn_greet.pack(side=tk.LEFT)

btn_exit = tk .Button(frame, text="Exit", bg="red",


fg='white', command=exit)
btn_exit.pack(side=tk.RIGHT)

root.mainloop()

B import tkinter as tk
def greet():
print("Tkinter GUI is fun...")

def exit():
root.destroy()

root = tk .Tk()
frame = tk .Frame(root)
frame.pack()

btn_greet = tk .Button(root, text="Hello", bg='green',


fg='white', command=greet)

btn_exit = tk .Button(root, text="Exit", bg="red",


fg='white', command=exit)

root.mainloop()

C import tkinter as tk

def greet():
print("Tkinter GUI is fun...")

def exit():
root.destroy()

root = tk .Tk()
frame = tk .Frame()

btn_greet = tk .Button(frame, text="Hello", bg='green',


fg='white', command=greet)
btn_greet.pack(side=tk.LEFT)
btn_exit = tk .Button(frame, text="Exit", bg="red",
fg='white', command=exit)
btn_exit.pack(side=tk.RIGHT)

root.mainloop()

D import tkinter as tk

def greet():
print("Tkinter GUI is fun...")

def exit():
root.destroy()

root = tk .Tk()
frame = tk .Frame(root)
frame.pack()

btn_greet = tk .Button(frame, text="Hello", bg='green',


fg='white', command=greet)
btn_greet.pack(side=tk.LEFT)

btn_exit = tk .Button(frame, text="Exit", bg="red",


fg='white', command=exit)
btn_exit.pack(side=tk.RIGHT)

root.mainloop()
ANSWERS OF THE FINAL EXAM QUESTIONS
Question Answer
Q1 C
Q2 B
Q3 D
Q4 A
Q5 B
Q6 C
Q7 A
Q8 D
Q9 C
Q10 B
Q11 D
Q12 A
Q13 B
Q14 C
Q15 D
Q16 A
Q17 C
Q18 B
Q19 C
Q20 D
13. Conclusion
This is the last chapter in this book. We have covered almost all
of the intermediate level concepts in Python and we covered them
in great detail. We started with setting our development
environment, the PyCharm IDE. Then we learned Exception
Handling, Modules & Packages, Format Operations and File
Operations in Python. Finally we covered one of the most
important topics in computer programming which is OOP (Object
Oriented Programming). We built two big projects that are the PDF
& CSV Operations and the Movie Library with Tkinter. Especially
in the Movie Library we build a real world project which you can
use as a reference in your programming life. You also had
assignments after the projects. And finally you got the Final Exam
to test yourself.

The approach we followed in this book is quite unique and


intense. It aims to teach you Python in a solid and unforgettable
way. That’s why we had more than 50 coding exercises, quizzes
and assignments. The idea is to make sure we used all the possible
ways to help you learn Python programming. And I hope, I
achieved this.

This book is a part of the Hands-On Python Series. There are


three books in this series: Beginner, Intermediate and Advanced.
This book is the second one, the Intermediate level. You can start
the Advanced part after you finish this book and feel comfortable
with the fundamentals of Python.
Dear reader!

I want to thank you with all my heart, for your interest in my


book, your patience and your desire to learn Python. You did a
great job finishing this intense book. And I am very happy to be a
part of this. I hope we see each other again in another programming
book. Till then, I hope, you have wonderful life and reach your
dreams.

Good bye.

Musa Arda

[1] The Beginner book in the series:


https://www.amazon.com/dp/B09JM2ZCKW
[2] Python official website: https://www.python.org/
[3] Python Virtual Environments:
https://docs.python.org/3/tutorial/venv.html
[4] PyCharm’s official website:
https://www.jetbrains.com/pycharm/
[5] PyCharm official documentation:
https://www.jetbrains.com/help/pycharm/quick-start-guide.html
[6] Official website for installing packages:
https://packaging.python.org/tutorials/installing-packages/

You might also like