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: python.hands.on.book@gmail.com

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 python.hands.on.book@gmail.com.

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

3

# Press Shift+F10 to execute it or replace it with your code.

4

# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.

5

6

7

def print_hi(name):

8

    # Use a breakpoint in the code line below to debug your script.

9

    print(f'Hi, {name}')  # Press Ctrl+F8 to toggle the breakpoint.

10

11

12

# Press the green button in the gutter to run the script.

13

if __name__ == '__main__':

14

    print_hi('PyCharm')

15

16

# See PyCharm help at 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 two 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'

4

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

5

print(a + b)

[6]:

TypeError: unsupported operand type(s) for +: 'int' 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

7

    # ValueError: invalid literal for int() with base 10: '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

6

    # ValueError: invalid literal for int() with base 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 user_input 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

6

    # ValueError: invalid literal for int() with base 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

5

    assert int(user_input), ValueError('Not asserted 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

4

    assert n2 != 0, 'You cannot divide by zero. - 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)

[23]:

FileNotFoundError: [Errno 2] No such file or 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

5

        dividend = int(input('Please enter a number to 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]))

[2]:

TypeError: unsupported operand type(s) for +=: 'int' 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]))

[3]:

TypeError: unsupported operand type(s) for +=: 'int' 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)

[9]:

FileNotFoundError: [Errno 2] No such file or 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

4

    assert type(a_list) == list, AssertionError("The 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]))

[2]:

TypeError: unsupported operand type(s) for +=: '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:

18

# this time, since we will check every element one 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():

3

    reviews = [{'Image': 4, 'Like': 20, 'Comment': 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:

11

        raise Exception('Please enter two operands and 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:

17

        raise Exception('{0} type, is not valid. It must 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

37

        return "{0} {1} {2} = {3}".format(num_1, 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)

[9]:

FileNotFoundError: [Errno 2] No such file or 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: ')

15

            print('number of seasons in {0} is: {1}'.format(name, series[name]))

16

        except:

17

            print('No such series in the dictionary. 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)

[6]:

<module 'platform' from '...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

2

print('platform architecture:', 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\site-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)

[17]:

{'sys': <module 'sys' (built-in)>, 'builtins': <module '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))

[19]:

TypeError: randint() takes 0 positional arguments 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'):

7

    return input('Please enter a/an {0}: '.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\site-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

5

user_input = modules.module_input_operations.get_input()

6

print(user_input)

[29]:

Please enter a/an text: This is from the module in a 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

8

user_input = local_module_input_operations.get_input()

9

print(user_input)

[30]:

Please enter a/an text: Project Level access to a 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.... /@*-'

8

cons = 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

6

cons = 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

11

cons = 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:

Format (%) operator (old style formatting)

str.format (new style formatting)

f-strings (String Interpolation)

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

2

info = 'Python released in %d and being used %.1f 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

}

7

file_info = " P: %(path)s \n V: %(version).1f \n A: %(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}'

3

language = text.format(n='Python', s="Monty 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}'

3

language = text.format(lang='Java', source='Java 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 'Java' 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']

3

print(f'{ info[0] } lives in { info[1] } and works for { info[2] }')

[14]:

Clark Kent lives in Metropolis and works for Daily 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'

4

s = "The capital city of {0} is {1}".format(country, capitals[country])

5

print(s)

[6]:

The capital city of Germany is Berlin

S7:

[7]:

1

# S 7:

2

3

country = 'UK'

4

s = f"The capital city of {country} is {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')

7

print(template.substitute(co=country, 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:

Binary Types: pdf, doc, jpg, png, mp4, zip, etc.

Text Types: txt, xml, html, csv, .py, .java, etc.

Encoding defines how data is stored in a file. Common encodings types are:

ASCII (128 characters)

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')

[4]:

FileNotFoundError: [Errno 2] No such file or 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

5

with open('words.txt', encoding='utf-8', mode='rt') as 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)

[10]:

b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\x02\x …

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

3

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

4

    content = lorem.read()

5

    print(content)

[11]:

Lorem ipsum dolor sit amet, consectetur adipiscing 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

2

with open('lorem_ipsum.txt', mode='rt', 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)

[13]:

Lorem ipsum dolor sit amet, consectetur adipiscing 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

2

with open('lorem_ipsum.txt', mode='rt', 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)

[14]:

Lorem ipsum dolor sit amet, consectetur adipiscing 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

2

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

3

    print(lorem)

[15]:

<_io.TextIOWrapper name='lorem_ipsum.txt' 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

2

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

3

    for line in lorem:

4

        print(line)

[16]:

Lorem ipsum dolor sit amet, consectetur adipiscing 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

3

with open('lorem_ipsum.txt', mode='rt', 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)

[17]:

Lorem ipsum dolor sit amet, consectetur adipiscing 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:

3

    list_to_append = ['\nMulti Line 1', '\nMulti Line 2', '\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

4

os.rename('file_to_rename.txt', 'file_with_new_name.txt')

[21]:

FileNotFoundError: [WinError 2] The system cannot 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:

4

    os.rename('file_to_rename.txt', '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:

3

    with open('file_to_create.txt', mode='x') as new_file:

4

        new_file.write('This is a new file with mode 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 os.DirEntry 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:

os:

os.mkdir(): creates a single directory

os.makedirs(): creates multiple directories

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)

[38]:

[WinError 183] Cannot create a file when that file 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()

2

p2 = 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:

String Methods: startswith, endswith, find, etc.

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

6

        if content.is_file() and 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():

9

        if f.is_file() and fnmatch.fnmatch(f.name, 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:

os.rmdir(): if the folder is empty

pathlib.Path.rmdir(): if the folder is empty

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

2

# OSError: [WinError 145] The directory is not empty: 'folder_to_delete_2'

3

pathlib.Path('folder_to_delete_2').rmdir()

[48]:

OSError: [WinError 145] The directory is not empty: '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:

shutil.copy(src, dst)

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:

The Great Wave off Kanagawa.jpg

Katsushika Hokusai - 1829

https://en.wikipedia.org/wiki/The_Great_Wave_off_Kanagawa

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

3

archive = zipfile.ZipFile('neural_style_transfer.zip', 'r')

4

print(archive)

[54]:

<zipfile.ZipFile filename='neural_style_transfer.zip' 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

4

shutil.make_archive('new_archive_folder', 'zip', '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 ---

[3]:

<_io.TextIOWrapper 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)

[3]:

<_io.TextIOWrapper 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)

7

os.makedirs('quiz_files/folder 10/sub folder 10', exist_ok=True)

8

os.makedirs('quiz_files/folder 10/sub folder 20', exist_ok=True)

9

10

# (pathlib.Path.mkdir)

11

Path('quiz_files/folder 20').mkdir(exist_ok=True)

12

Path('quiz_files/folder 30/sub folder 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()

11

         if file.is_file() and fnmatch.fnmatch(file.name, 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

5

shutil.make_archive('quiz_folder_archive', 'zip', 'quiz_files')

7.      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 dialect 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

2

movie_to_add = ["250","Slumdog Millionaire","2008","R","25 Dec 2008",

3

                "120 min","Drama", "Danny Boyle, Loveleen Tandan",

4

                "Simon Beaufoy (screenplay), Vikas Swarup (novel)",

5

                "Dev Patel, Saurabh Shukla, Anil Kapoor, Raj Zutshi",

6

                "A Mumbai teen reflects on his upbringing in the ...",

7

                "English, Hindi, French","UK, France, USA",

8

                "Won 8 Oscars. Another 144 wins & 126 nominations.",

9

                "https://images-na.ssl-images-amazon.com/...jpg",

10

                "Internet Movie 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

22

        writer = csv.writer(file, delimiter=',', 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, \

11

         open(new_movie_path, 'a', newline='') as movies_copy:

12

        # reader from the first one

13

        movies_to_copy = csv.reader(movies)

14

15

        # writer for the second one

16

        writer = csv.writer(movies_copy, quoting=csv.QUOTE_ALL)

17

18

        # loop over movies to copy one by one -> write 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()

9

pdf_path = os.path.join(project_path, 'pdf', '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()

[16]:

<PyPDF2.pdf.PdfFileReader object at 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 pdf 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

6

    from PyPDF2 import PdfFileReader, PdfFileWriter

7

8

    # define a path for extracted pdf

9

    new_pdf_path = os.path.join(project_path, 'pdf', 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

4

    from PyPDF2 import PdfFileReader, PdfFileWriter

5

6

    # ex: 2_8.pdf

7

    new_pdf_path = os.path.join(project_path, 'pdf', 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

15

    merged_pdf_path = os.path.join(project_path, 'pdf', 'merged_pdf.pdf')

16

17

    # now, fill the new pdf with the merger

18

    with open(merged_pdf_path, 'wb') as 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)

13

    pdf_operations.pdf_merge('pdf/8.pdf', '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

12

    from PyPDF2 import PdfFileReader, PdfFileWriter

13

14

    # read the merged_pdf

15

    merged_pdf = 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)

13

    # pdf_operations.pdf_merge('pdf/8.pdf', '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.

8.      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 dialect 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

6

        #TODO use Sniffer class in csv to see if the content has valid header (has_header)

7

8

        print('CSV Has Valid Reader:', has_reader)

9

10

        dialect = #TODO call sniff method of Sniffer 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

2

movie_to_add = ["250","Slumdog Millionaire","2008","R","25 Dec 2008",

3

                "120 min","Drama", "Danny Boyle, Loveleen Tandan",

4

                "Simon Beaufoy (screenplay), Vikas Swarup (novel)",

5

                "Dev Patel, Saurabh Shukla, Anil Kapoor, Raj Zutshi",

6

                "A Mumbai teen reflects on his upbringing in the ...",

7

                "English, Hindi, French","UK, France, USA",

8

                "Won 8 Oscars. Another 144 wins & 126 nominations.",

9

                "https://images-na.ssl-images-amazon.com/...jpg",

10

                "Internet Movie 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

    #TODO open movie_path in append mode and make sure you understand newline parameter:

20

21

        # get a csv writer

22

        writer = #TODO get a csv writer with appropriate 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

11

    with #TODO open movie_path in read mode, #TODO open new_movie_path in append mode:

12

13

        # reader from the first one

14

        movies_to_copy = #TODO get csv reader for movies

15

16

        # writer for the second one

17

        writer = #TODO get csv writer for movies_copy

18

19

        # loop over movies to copy one by one -> write into the new file

20

        #TODO loop over movies_to_copy and write 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()

10

pdf_path = os.path.join(project_path, 'pdf', '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 = #TODO call getNumPages() on 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()

[16]:

<PyPDF2.pdf.PdfFileReader object at 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 = #TODO get page from pdf.pages at page_num index

7

        content = #TODO extract page text (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 pdf 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:

5

        for #TODO loop over range from start to end (inc):

6

            print(f'---------- start of page {i} ----------')

7

8

            page = #TODO get page i from pdf

9

            content = #TODO extract page text (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

7

    #TODO import PdfFileReader, PdfFileWriter from PyPDF2 package

8

9

    new_pdf_path = os.path.join(project_path, 'pdf', 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

18

    pdf_writer = #TODO instantiate PdfFileWriter object

19

    #TODO add page to pdf_writer

20

21

    # now, fill the new pdf with the pdf_writer

22

    # wb -> write binary

23

    with #TODO open new_pdf_path in write-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

5

    #TODO import PdfFileReader, PdfFileWriter from PyPDF2 package

6

7

    # ex: 2_8.pdf

8

    new_pdf_path = os.path.join(project_path, 'pdf', str(start) + '_' + str(end) + '.pdf')

9

10

    # get the PDF

11

    pdf = #TODO get PdfFileReader with pdf_path

12

13

    # create new pdf

14

    pdf_writer = #TODO instantiate PdfFileWriter object

15

16

    # add pages

17

    for #TODO loop over range from start to end (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

26

    with #TODO open new_pdf_path in write-binary mode:

27

        #TODO write the pdf_writer into 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

6

    # TODO import PdfFileMerger from PyPDF2 package

7

8

    # merger object

9

    merger = #TODO instantiate PdfFileMerger object

10

11

    # *args

12

    #TODO loop over args and append into merger

13

14

    # save merger as pdf

15

    merged_pdf_path = os.path.join(project_path, 'pdf', 'merged_pdf.pdf')

16

17

    with #TODO open merged_pdf_path in write-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)

13

    pdf_operations.pdf_merge('pdf/8.pdf', '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

12

    #TODO import PdfFileReader, PdfFileWriter from PyPDF2 package

13

14

    # read the merged_pdf

15

    merged_pdf = #TODO read 'pdf/merged_pdf.pdf' with PdfFileReader

16

17

    # create a writer

18

    writer = #TODO instantiate PdfFileWriter object

19

20

    # loop over the pages

21

    for #TODO loop over number of pages in merged_pdf:

22

23

        # get the page

24

        page = #TODO get page i in merged_pdf

25

26

        # rotate the page

27

        #TODO rotate page Clockwise in the given degree

28

29

        # pass this page to writer

30

        #TODO add this page to writer

31

32

    with #TODO open 'pdf/rotated.pdf' in write-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)

13

    # pdf_operations.pdf_merge('pdf/8.pdf', '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')

3

yellow_eyed = Penguin('Yellow-Eyed Penguin', 1, '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

2

print('Scientific Family of {0} is {1}'.format(king.name, king.__class__.family))

3

print('Scientific Family of {0} is {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))

3

print('Scientific Family of {0} is {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

2

print('Age of {0} is {1}, and its color is {2}'.format(king.name, king.age, king.color))

3

print('Age of {0} is {1}, and its color is {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

4

    # We write parent class/classes inside parenthesis

5

6

    def __init__(self):

7

        # first -> we call __init__() of Super class (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()

[15]:

AttributeError: 'Bird' object has no attribute '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):

4

        # let's define the standard price of the telephone -> private attribute

5

        self.__price = 1000

6

7

    def sell(self):

8

        print('Selling price is: ${}'.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

[18]:

AttributeError: 'Telephone' object has no attribute '__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)

3

# again we see as $1000 -> because we didn't set the __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):

3

    # We write parent class/classes inside parenthesis

4

5

    def __init__(self):

6

        # first -> we call __init__() of Super class (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):

3

    # We write parent class/classes inside 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()

[39]:

TypeError: Car.work() missing 1 required positional 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)

[50]:

NameError: name 'std' is not defined. Did you mean: '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

2

print('Is Shape a subclass of object class?: ', 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

3

print('Is Square class is subclass of Shape class?: ', 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

2

# Clock and Calendar -> CalendarClock will 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):

19

        return '{0}:{1}:{2}'.format(self.__hours, 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):

14

        return '{d}:{m}:{y}'.format(d=self.__d, 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

5

    def __init__(self, day, month, year, hours, minutes, 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

2

calendar_clock = CalendarClock(8, 12, 2020, 16, 45, 7)

3

print(calendar_clock)

4

print(calendar_clock.time())

5

print(calendar_clock.date())

[70]:

<_4_multiple_inheritance.CalendarClock object at 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):

15

        return '{0}:{1}:{2}'.format(self.__hours, 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):

14

        return '{d}:{m}:{y}'.format(d=self.__d, 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

5

    def __init__(self, day, month, year, hours, minutes, 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

2

calender_clock = CalendarClock(12, 1, 2020, 13, 25, 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):

10

        return 'This is a point at coordinates: ({0},{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

9

album_1 = Album('Yesterday and Today', 'The 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:

3

    "This class represents a rectangle on (x,y) 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):

13

        self.width = self.corner_1.distance(self.corner_2)

14

15

    def calculate_length(self):

16

        self.length = 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:

11

            print('Sorry, you can not withdraw this 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:

3

    def __init__(self, name, artist, album, 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

24

        song = Song(song_name, self.artist, self, 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

33

album = Album('Yesterday and Today', 'The Beatles', 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

15

album_1 = Album('Yesterday and Today', 'The 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

14

        song = Song(song_name, self.artist, self, 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

28

album_1 = Album('Yesterday and Today', 'The 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:

data: it will contain data files like colors.py, geometry.py, imdb_top_250.csv and menus.py

images: it will contain folders like home, posters_large and posters_small for movie images

mockup: it will contain the mockup images of the project

page: it is a package that contains the page folders as home, movie_detail and movie_list

tkinter_basics: it contains basic widgets to learn Tkinter

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

#

9

 # WIDGETS -> Controls (Buttons, Text, Label, 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',

14

                # text-unit -> width (width of one char of 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:

master: this represents the parent widget, which is the main window in our case: master=window

text: the text to display on the button

width & height: the width and height of the button (in text units)

bg: background color of the button

fg: foreground (text) color of the button

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

12

chkBtn_1 = tk.Checkbutton(master=window, text='Correct')

13

chkBtn_2 = tk.Checkbutton(master=window, 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:

master: this represents the parent widget, which is the main window in our case: master=window

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

18

btnLeft = Button(master=frame, text='Frame Button Left', bg='black', fg='white')

19

btnLeft.pack(side=tk.LEFT)

20

21

btnRight = Button(master=frame, text='Frame 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:

Root Window which is the main window and the class for it is: Window

Left Frame is left side of the screen and the class is: LeftFrame

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):

24

        w, h = GEOMETRY.MAIN_WINDOW_WIDTH, 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):

22

        self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=(62, 0))

23

24

    def add_button(self):

25

        btn = tk.Button(master=self.frame, text='Left 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

11

    left_frame = LeftFrame(root.window, '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):

23

        self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=(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 __init__() 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 __init__() 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

32

    # method for click event of menu buttons -> 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':

40

                button = Button(self.frame, menu_key, menu_text,

41

                                COLORS.ORANGE, COLORS.BLACK,

42

                                18, 2,

43

                                handle_click=self.handle_click,

44

                                side=tk.BOTTOM)

45

            else:

46

                button = Button(self.frame, menu_key, menu_text,

47

                                COLORS.ORANGE, 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

54

        # all the menu buttons -> event.widget.master.children

55

        for child in event.widget.master.winfo_children():

56

            if child == event.widget:

57

                child.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

58

            else:

59

                child.configure(bg=COLORS.BLACK, 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):

38

        # in tkinter -> .bind() -> left mouse click -> <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

12

    def __init__(self, window, name, 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()

25

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

12

from .right_frame.RightFrame import 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

4

from widget import Window, LeftFrame, RightFrame

5

6

if __name__ == '__main__':

7

    # Root Window

8

    root = Window('Movie Library')

9

10

    # LEFT FRAME

11

    left_frame = LeftFrame(root.window, 'leftFrame')

12

13

    # RIGHT FRAME

14

    right_frame = RightFrame(root.window, '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):

23

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

14

    def __init__(self, window, name, 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()

27

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

10

    def __init__(self, window, bg_color, 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):

23

        self.frame.pack(side=self.side, fill=tk.BOTH, 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,

43

                              text='Capstone Project - Movie 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):

50

        load = Image.open('images/home/python_logo.png')

51

        render = ImageTk.PhotoImage(load)

52

        img_lb = tk.Label(self.frame, image=render, 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

11

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

12

13

    def __init__(self, master, bg_color, 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')

26

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

27

28

    def add_page_title(self, title):

29

        lbl = tk.Label(self.frame, text=title, height=3,

30

                       bg=COLORS.BLACK, fg=COLORS.WHITE, font=('Arial', 12, 'bold'))

31

        lbl.grid(row=0, column=0, columnspan=8, 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

33

    # method for click event of menu buttons -> 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

46

        RightFrame.frame_content(rightFrame, 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':

52

                button = Button(self.frame, menu_key, menu_text,

53

                                COLORS.ORANGE, COLORS.BLACK,

54

                                18, 2,

55

                                handle_click=self.handle_click,

56

                                side=tk.BOTTOM)

57

            else:

58

                button = Button(self.frame, menu_key, menu_text,

59

                                COLORS.ORANGE, 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

66

        # all the menu buttons -> event.widget.master.children

67

        for child in event.widget.master.winfo_children():

68

            if child == event.widget:

69

                child.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

70

            else:

71

                child.configure(bg=COLORS.BLACK, 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

14

    def __init__(self, window, name, 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()

27

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

37

                Home(incoming_frame, RightFrame.bg_color)

38

            elif page_name == 'movieList':

39

                # add movie list page

40

                MovieList(incoming_frame, RightFrame.bg_color)

41

            elif page_name == 'movieDetail':

42

                # add movie detail page

43

                pass

44

45

    # static method -> Class Method (no self 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:

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.)

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

12

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

13

14

    def __init__(self, master, bg_color, 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()

29

        self.frame.pack(side=self.side, fill=tk.BOTH, 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'))

35

        lbl.grid(row=0, column=0, columnspan=8, 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:

41

            movie_dict = csv.DictReader(file, 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):

49

        for j, column in enumerate(MovieList.columns):

50

            if column != 'imdbID':

51

                lbl = tk.Label(self.frame, text=str(column),

52

                               width=54, height=2, 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':

63

                    lbl.configure(text='# of Ratings', width=12)

64

65

                # place in a grid

66

                if column == 'imdbVotes':

67

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

68

                else:

69

                    lbl.grid(row=1, column=j, sticky='we', 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

12

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

13

14

    def __init__(self, master, bg_color, 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()

29

        self.frame.pack(side=self.side, fill=tk.BOTH, 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'))

35

        lbl.grid(row=0, column=0, columnspan=8, 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:

41

            movie_dict = csv.DictReader(file, 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):

50

        for j, column in enumerate(MovieList.columns):

51

            if column != 'imdbID':

52

                lbl = tk.Label(self.frame, text=str(column),

53

                               width=54, height=2, 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':

64

                    lbl.configure(text='# of Ratings', width=12)

65

66

                # place in a grid

67

                if column == 'imdbVotes':

68

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

69

                else:

70

                    lbl.grid(row=1, column=j, sticky='we', 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):

75

                name = 'table_row_' + str(i) + str(j) + '_' + 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

86

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

87

        except:

88

            # load no image

89

            load = 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, bg=COLORS.ORANGE)

93

            lbl_img.image = render

94

            lbl_img.grid(row=i + 2, column=j, padx=(7, 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

12

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

13

14

    def __init__(self, master, bg_color, 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()

29

        self.frame.pack(side=self.side, fill=tk.BOTH, 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'))

35

        lbl.grid(row=0, column=0, columnspan=8, 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:

41

            movie_dict = csv.DictReader(file, 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):

50

        for j, column in enumerate(MovieList.columns):

51

            if column != 'imdbID':

52

                lbl = tk.Label(self.frame, text=str(column),

53

                               width=54, height=2, 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':

64

                    lbl.configure(text='# of Ratings', width=12)

65

66

                # place in a grid

67

                if column == 'imdbVotes':

68

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

69

                else:

70

                    lbl.grid(row=1, column=j, sticky='we', 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):

75

                name = 'table_row_' + str(i) + str(j) + '_' + 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

86

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

87

        except:

88

            # load no image

89

            load = Image.open('images/posters_small/no_image.jpg')

90

        finally:

91

            render = ImageTk.PhotoImage(load)

92

            lbl_img = tk.Label(self.frame, name=name,

93

                               image=render, bg=COLORS.ORANGE)

94

            lbl_img.image = render

95

            lbl_img.grid(row=i + 2, column=j, padx=(7, 0), sticky='we')

96

97

    def write_label(self, movie, i, j, key, name):

98

        lbl = tk.Label(self.frame, name=name, text=str(movie[key]),

99

                       height=4, bg=COLORS.WHITE, fg=COLORS.BLACK,

100

                       font=('Arial', 10, 'bold'), 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':

116

            lbl.grid(row=i + 2, column=j, sticky='we', padx=(0, 10))

117

        else:

118

            lbl.grid(row=i + 2, column=j, sticky='we', 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

18

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

19

20

    def __init__(self, master, bg_color, 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()

35

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

36

37

    def add_page_title(self, title):

38

        lbl = tk.Label(self.frame, text=title, height=3,

39

                       bg=COLORS.BLACK, fg=COLORS.WHITE,

40

                       font=('Arial', 12, 'bold'))

41

        lbl.grid(row=0, column=0, columnspan=8, 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:

47

            movie_dict = csv.DictReader(file, delimiter=';')

48

            for movie in movie_dict:

49

                self.movies.append(movie)

50

51

        # assign total_num_pages

52

        MovieList.total_num_pages = len(self.movies) // 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):

60

        for j, column in enumerate(MovieList.columns):

61

            if column != 'imdbID':

62

                lbl = tk.Label(self.frame, text=str(column),

63

                               width=54, height=2, 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':

74

                    lbl.configure(text='# of Ratings', width=12)

75

76

                # place in a grid

77

                if column == 'imdbVotes':

78

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

79

                else:

80

                    lbl.grid(row=1, column=j, sticky='we', 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

86

            start_page_num = (MovieList.page_number - 1) * MovieList.movies_per_page

87

            end_page_num = MovieList.page_number * MovieList.movies_per_page

88

            if start_page_num <= i < end_page_num:

89

                for j, key in enumerate(MovieList.columns):

90

                    name = 'table_row_' + str(i) + str(j) + '_' + 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

104

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

105

        except:

106

            # load no image

107

            load = Image.open('images/posters_small/no_image.jpg')

108

        finally:

109

            render = ImageTk.PhotoImage(load)

110

            lbl_img = tk.Label(self.frame, name=name,

111

                               image=render, 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):

116

        lbl = tk.Label(self.frame, name=name, text=str(movie[key]),

117

                       height=4, bg=COLORS.WHITE, fg=COLORS.BLACK,

118

                       font=('Arial', 10, 'bold'), 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':

137

            lbl.grid(row=i + 2, column=j, sticky='we', padx=(0, 10))

138

        else:

139

            lbl.grid(row=i + 2, column=j, sticky='we', 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

155

        values = list(range(1, MovieList.total_num_pages))

156

        pages = ttk.Combobox(self.frame, 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

161

        pages.bind('<<ComboboxSelected>>', self.create_combo_box_select_event)

162

        pages.grid(row=self.i, column=2, pady=(15, 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

18

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

19

20

    def __init__(self, master, bg_color, 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()

35

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

36

37

    def add_page_title(self, title):

38

        lbl = tk.Label(self.frame, text=title, height=3,

39

                       bg=COLORS.BLACK, fg=COLORS.WHITE,

40

                       font=('Arial', 12, 'bold'))

41

        lbl.grid(row=0, column=0, columnspan=8, 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:

47

            movie_dict = csv.DictReader(file, delimiter=';')

48

            for movie in movie_dict:

49

                self.movies.append(movie)

50

51

        # assign total_num_pages

52

        MovieList.total_num_pages = len(self.movies) // 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):

60

        for j, column in enumerate(MovieList.columns):

61

            if column != 'imdbID':

62

                lbl = tk.Label(self.frame, text=str(column),

63

                               width=54, height=2, 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':

74

                    lbl.configure(text='# of Ratings', width=12)

75

76

                # place in a grid

77

                if column == 'imdbVotes':

78

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

79

                else:

80

                    lbl.grid(row=1, column=j, sticky='we', 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

86

            start_page_num = (MovieList.page_number - 1) * MovieList.movies_per_page

87

            end_page_num = MovieList.page_number * MovieList.movies_per_page

88

            if start_page_num <= i < end_page_num:

89

                for j, key in enumerate(MovieList.columns):

90

                    name = 'table_row_' + str(i) + str(j) + '_' + 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

104

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

105

        except:

106

            # load no image

107

            load = Image.open('images/posters_small/no_image.jpg')

108

        finally:

109

            render = ImageTk.PhotoImage(load)

110

            lbl_img = tk.Label(self.frame, name=name,

111

                               image=render, 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):

116

        lbl = tk.Label(self.frame, name=name, text=str(movie[key]),

117

                       height=4, bg=COLORS.WHITE, fg=COLORS.BLACK,

118

                       font=('Arial', 10, 'bold'), 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':

137

            lbl.grid(row=i + 2, column=j, sticky='we', padx=(0, 10))

138

        else:

139

            lbl.grid(row=i + 2, column=j, sticky='we', 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

        MovieList.page_number = 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

159

        values = list(range(1, MovieList.total_num_pages))

160

        pages = ttk.Combobox(self.frame, 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

165

        pages.bind('<<ComboboxSelected>>', self.create_combo_box_select_event)

166

        pages.grid(row=self.i, column=2, pady=(15, 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

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

33

    # method for click event of menu buttons -> 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

46

        RightFrame.frame_content(rightFrame, 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':

52

                button = Button(self.frame, menu_key, menu_text,

53

                                COLORS.ORANGE, COLORS.BLACK,

54

                                18, 2,

55

                                handle_click=self.handle_click,

56

                                side=tk.BOTTOM)

57

            else:

58

                button = Button(self.frame, menu_key, 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

69

        # all the menu buttons -> event.widget.master.children

70

        for child in event.widget.master.winfo_children():

71

            if child == event.widget:

72

                child.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

73

            else:

74

                child.configure(bg=COLORS.BLACK, fg=COLORS.ORANGE)

75

76

    def selected_button_color(self, button):

77

        # tk button configure

78

        button.configure(bg=COLORS.ORANGE, 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

7

    def __init__(self, window, bg_color, 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

10

from .movie_detail.MovieDetail import 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

8

from page.movie_detail.MovieDetail import 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

19

    columns = ['imdbID', 'Id', 'Title', 'Year', 'imdbRating', 'imdbVotes']

20

21

    def __init__(self, master, bg_color, 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()

36

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

37

38

    def add_page_title(self, title):

39

        lbl = tk.Label(self.frame, text=title, height=3,

40

                       bg=COLORS.BLACK, fg=COLORS.WHITE,

41

                       font=('Arial', 12, 'bold'))

42

        lbl.grid(row=0, column=0, columnspan=8, 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:

48

            movie_dict = csv.DictReader(file, delimiter=';')

49

            for movie in movie_dict:

50

                self.movies.append(movie)

51

52

        # assign total_num_pages

53

        MovieList.total_num_pages = len(self.movies) // 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):

61

        for j, column in enumerate(MovieList.columns):

62

            if column != 'imdbID':

63

                lbl = tk.Label(self.frame, text=str(column),

64

                               width=54, height=2, 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':

75

                    lbl.configure(text='# of Ratings', width=12)

76

77

                # place in a grid

78

                if column == 'imdbVotes':

79

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

80

                else:

81

                    lbl.grid(row=1, column=j, sticky='we', 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

87

            start_page_num = (MovieList.page_number - 1) * MovieList.movies_per_page

88

            end_page_num = MovieList.page_number * MovieList.movies_per_page

89

            if start_page_num <= i < end_page_num:

90

                for j, key in enumerate(MovieList.columns):

91

                    name = 'table_row_' + str(i) + str(j) + '_' + 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

105

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

106

        except:

107

            # load no image

108

            load = Image.open('images/posters_small/no_image.jpg')

109

        finally:

110

            render = ImageTk.PhotoImage(load)

111

            lbl_img = tk.Label(self.frame, name=name,

112

                               image=render, bg=COLORS.ORANGE)

113

            lbl_img.image = render

114

            lbl_img.grid(row=i + 2, column=j, padx=(7, 0), sticky='we')

115

116

    def write_label(self, movie, i, j, key, name):

117

        lbl = tk.Label(self.frame, name=name, text=str(movie[key]),

118

                       height=4, bg=COLORS.WHITE, fg=COLORS.BLACK,

119

                       font=('Arial', 10, 'bold'), 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':

141

            lbl.grid(row=i + 2, column=j, sticky='we', padx=(0, 10))

142

        else:

143

            lbl.grid(row=i + 2, column=j, sticky='we', 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):

153

        MovieList.page_number = 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

163

        values = list(range(1, MovieList.total_num_pages))

164

        pages = ttk.Combobox(self.frame, 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

169

        pages.bind('<<ComboboxSelected>>', self.create_combo_box_select_event)

170

        pages.grid(row=self.i, column=2, pady=(15, 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

184

        # modify the right frame (clear the right 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':

207

                        ch.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

208

                    else:

209

                        ch.configure(bg=COLORS.BLACK, 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

8

    def __init__(self, window, bg_color, 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:

30

            lbl = tk.Label(self.frame, text=self.movie['Title'], height=3,

31

                           bg=COLORS.BLACK, fg=COLORS.WHITE, font=('Arial', 12, 'bold'))

32

            lbl.grid(row=0, column=0, columnspan=8, padx=1, pady=(0, 8), sticky='we')

33

        else:

34

            lbl = tk.Label(self.frame, text=title, height=3,

35

                           bg=COLORS.BLACK, 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

50

            load = Image.open('images/posters_large/' + str(self.imdbID) + '.jpg')

51

        except:

52

            # load no image

53

            load = Image.open('images/posters_large/no_image.jpg')

54

        finally:

55

            render = ImageTk.PhotoImage(load)

56

            lbl_img = tk.Label(self.frame, image=render, bg=COLORS.ORANGE)

57

            lbl_img.image = render

58

            lbl_img.grid(row=1, column=0, 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)

69

            lbl = tk.Label(self.frame, text=txt, height=2, 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)

83

            lbl = tk.Label(self.frame, text=txt, height=2, 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

14

    def __init__(self, window, name, 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()

27

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

37

                Home(incoming_frame, RightFrame.bg_color)

38

            elif page_name == 'movieList':

39

                # add movie list page

40

                MovieList(incoming_frame, RightFrame.bg_color)

41

            elif page_name == 'movieDetail':

42

                # add movie detail page

43

                MovieDetail(incoming_frame, RightFrame.bg_color)

44

45

    # static method -> Class Method (no self 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.

11.        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:

data: it will contain data files like colors.py, geometry.py, imdb_top_250.csv and menus.py

images: it will contain folders like home, posters_large and posters_small for movie images

mockup: it will contain the mockup images of the project

page: it is a package that contains the page folders as home, movie_detail and movie_list

tkinter_basics: it contains basic widgets to learn Tkinter

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

13

from widget import Window, LeftFrame, RightFrame

14

15

if __name__ == '__main__':

16

17

    # Root Window

18

    # TODO - Create the root object by calling Window class

19

20

    # LEFT FRAME

21

    # TODO - Create the left frame by calling LeftFrame class

22

23

    # RIGHT FRAME

24

    # TODO - Create the right frame by calling 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:

Root Window which is the main window and the class for it is: Window

Left Frame is left side of the screen and the class is: LeftFrame

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):

24

        w, h = GEOMETRI.MAIN_WINDOW_WIDTH, 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

9

from widget.right_frame.RightFrame import RightFrame

10

11

class LeftFrame:

12

    """

13

    Class for Left Frame to hold the menu items.

14

    """

15

16

    # TODO - define the init method for the LeftFrame

17

18

    def add_frame(self):

19

        self.frame.pack(side=tk.LEFT, fill=tk.Y, pady=(62, 0))

20

21

    # method for click event of menu buttons -> event handler

22

    def handle_click(self, event):

23

        self.manage_button_colors(event)

24

        page_name = str(event.widget).split('.')[2]

25

        # print('Page {0} is 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

32

        # TODO - call destroy_children() method of RightFrame

33

34

        # add new page -> page_name

35

        # TODO - call frame_content() method of 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':

41

                button = # TODO - create the Button 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

50

        # all the menu buttons -> event.widget.master.children

51

        for child in event.widget.master.winfo_children():

52

            if child == event.widget:

53

                child.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

54

            else:

55

                # TODO - configure the child as background = BLACK and foreground = ORANGE

56

57

    def selected_button_color(self, button):

58

        # tk button configure

59

        # TODO - configure the button as background = 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):

30

        # in tkinter -> .bind() -> left mouse click -> <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()

19

        self.frame.pack(side=self.side, fill=tk.BOTH, 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

29

                Home(incoming_frame, RightFrame.bg_color)

30

            # TODO - create other pages

31

32

    # static method -> Class Method (no self 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

12

from .right_frame.RightFrame import 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):

13

        self.frame.pack(side=self.side, fill=tk.BOTH, 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,

33

                              text='Capstone Project - Movie 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):

40

        load = Image.open('images/home/python_logo.png')

41

        render = ImageTk.PhotoImage(load)

42

        img_lb = tk.Label(self.frame, image=render, 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

9

from page.movie_detail.MovieDetail import 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

20

    columns = ['imdbID', 'Id', 'Title', 'Year', '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()

28

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

29

30

    def add_page_title(self, title):

31

        lbl = tk.Label(self.frame, text=title, height=3,

32

                       bg=COLORS.BLACK, fg=COLORS.WHITE, font=('Arial', 12, 'bold'))

33

        lbl.grid(row=0, column=0, columnspan=8, 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

41

        MovieList.total_num_pages = len(self.movies) // 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):

49

        for j, column in enumerate(MovieList.columns):

50

            if column != 'imdbID':

51

                lbl = tk.Label(self.frame, 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':

61

                    lbl.configure(text='# of Ratings', width=12)

62

63

                # place in a grid

64

                if column == 'imdbVotes':

65

                    lbl.grid(row=1, column=j, sticky='we', padx=(0, 10))

66

                else:

67

                    lbl.grid(row=1, column=j, sticky='we', 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

73

            if (MovieList.page_number - 1) * MovieList.movies_per_page <= i < MovieList.page_number * MovieList.movies_per_page:

74

                for j, key in enumerate(MovieList.columns):

75

                    name = 'table_row_' + str(i) + str(j) + '_' + 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

88

            load = Image.open('images/posters_small/' + movie['imdbID'] + '.jpg')

89

        except:

90

            # load no image

91

            load = Image.open('images/posters_small/no_image.jpg')

92

        finally:

93

            render = ImageTk.PhotoImage(load)

94

            lbl_img = tk.Label(self.frame, name=name, image=render, bg=COLORS.ORANGE)

95

            lbl_img.image = render

96

            lbl_img.grid(row=i+2, column=j, padx=(7,0), sticky='we')

97

98

    def write_label(self, movie, i, j, key, name):

99

        lbl = tk.Label(self.frame, name=name, text=str(movie[key]), height=4, bg=COLORS.WHITE, fg=COLORS.BLACK,

100

                       font=('Arial', 10, 'bold'), 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

108

        # TODO - modify background color for odd and even lines

109

110

        # place in a grid

111

        if key == 'imdbVotes':

112

            lbl.grid(row=i+2, column=j, sticky='we', padx=(0, 10))

113

        else:

114

            lbl.grid(row=i+2, column=j, sticky='we', 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):

124

        MovieList.page_number = 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

134

        values = list(range(1, MovieList.total_num_pages))

135

        pages = ttk.Combobox(self.frame, 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

140

        pages.bind('<<ComboboxSelected>>', 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

154

        # modify the right frame (clear the right 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':

176

                        ch.configure(bg=COLORS.ORANGE, fg=COLORS.WHITE)

177

                    else:

178

                        ch.configure(bg=COLORS.BLACK, 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')

15

        self.frame.pack(side=self.side, fill=tk.BOTH, expand=True)

16

17

    def add_page_title(self, title):

18

        if self.imdbID != None:

19

            lbl = tk.Label(self.frame, text=self.movie['Title'], height=3,

20

                           bg=COLORS.BLACK, fg=COLORS.WHITE, font=('Arial', 12, 'bold'))

21

            lbl.grid(row=0, column=0, columnspan=8, padx=1, pady=(0, 8), sticky='we')

22

        else:

23

            lbl = tk.Label(self.frame, text=title, height=3,

24

                       bg=COLORS.BLACK, 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):

41

        # TODO - assign the incoming movie to 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)

46

            lbl = tk.Label(self.frame, text=txt, height=2, 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

10

from .movie_detail.MovieDetail import 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 Handling

3

Modules & Packages

3

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?

A

In Python, a package is an ordinary Python file ending with '.pck' extension.

B

We use 'include' keyword to import Python packages.

C

In Python, packages are containers that hold modules.

D

In Python, a package is just an ordinary folder. To create a 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.

A

[print(f"Capital of {co} is {ci}.") for co, ci in 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()]

D

[print(f"Capital of {a} is {b}.") for a, b in 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

Y Z

B

X Y

C

X Z

D

Y X

Q17:

Which one below is not correct about OOP (Object Oriented Programming)?

A

Class is the blueprint that defines how the object should be created.

B

If we do not want anyone to access the attributes in the class, we define these attributes as private. This behavior is called Encapsulation.

C

When a class inherits from a parent class, it cannot modify 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/