Python Bookcamp Exercises and Hands-on Projects Vaskaran Sarcar Python Bookcamp: Exercises and Hands-on Projects Copyright © 2021 by Vaskaran Sarcar This work is subject to copyright. All rights are reserved by the Publisher, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and transmission or information storage and retrieval, electronic adaptation, computer software, or by similar or dissimilar methodology now known or hereafter developed. ISBN-13: 979-8581409275 ASIN: B08FTD48NF Trademarked names, logos, and images may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, logo, or image we use the names, logos, and images only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. The use in this publication of trade names, trademarks, service marks, and similar terms, even if they are not identified as such, is not to be taken as an expression of opinion as to whether or not they are subject to proprietary rights. While the advice and information in this book are believed to be true and accurate at the date of publication, neither the author nor the editor nor the publisher can accept any legal responsibility for any errors or omissions that may be made. The author and publisher make no warranty, express or implied, for the material contained herein. For information on translations, please e-mail vaskaran@rediffmail.com. Any source code or other supplementary material referenced by the author in this book is available to readers on GitHub via the book's product page, located at https://github.com/Vaskaran/PythonBookcamp. vaskaran@rediffmail.com For more detailed information, please email I dedicate this book to all the unsung heroes and volunteers who are fighting at the front lines of Covid19 battle to save humanity and this beautiful world. Contents About the Author Acknowledgments Preface Chapter 1: Getting Ready for Program Execution Overview Setting Up the Programming Environment Using Command Prompt Using IDLE Using PyCharm IDE Executing Simple Programs Demonstration 1 Output Demonstration 2 Output Demonstration 3 Output Chapter 2: Comments, Variables, and Operators Using Comments Demonstration 1 Output Analysis Introduction to Variables Strings vs Numbers int vs float Demonstration 2 Output Analysis Naming Conventions Assigning Multiple Variables in a Single Line Assigning Same Value to Multiple Variables Operators Arithmetic Operators Assignment Operators Comparison (or Relational) Operators Logical Operators Bitwise Operators Precedence of Operators Operators Associativity Identity Operators Membership Operators Solution to Exercises General Comments for Case Studies Case Study I Chapter 3: Common Data Types Code Demonstrations with Strings Code Demonstrations with Numbers Quick talk on f-strings Accepting User Inputs Demonstration 1 Output Solution to Exercises CS 1.1 Implementation Case Study II Chapter 4: Decision Making Using an if Statement Demonstration 1 Output Using the if-else Statements Demonstration 2 Output Demonstration 3 Output Demonstration 4 Output Using elif with an if-else Statement Demonstration 5 Output Demonstration 6 Output Analysis Tautology and Contradictions Alternative Designs Demonstration 7 Output Analysis Demonstration 8 Output Solution to Exercises CS 2.1 Implementation Case Study III Chapter 5: Iteration using Loops The Purpose of Iteration The while Loop Demonstration 1 Output Demonstration 2 Output Analysis The for Loop Demonstration 3 Output Analysis Demonstration 4 Output Analysis Use of break Statement Demonstration 5 Output Analysis Use of continue Statement Demonstration 6 Output Analysis Nested Loop Demonstration 7 Output Analysis Solution to Exercises CS 3.1 Implementation Case Study IV Chapter 6: Advanced Data Types Working with Lists Working with Tuples Working with Dictionaries Working with Sets Solution to Exercises CS 4.1 Implementation Possible improvements Case Study V Chapter 7: Functions and Modules Function Overview Demonstration 1 Output Analysis Argument vs Parameter Discussion on Function Arguments Positional Argument Keyword Arguments Use of Default Values Demonstration 2 Output Demonstration 3 Output Analysis Discussion on Return Values Demonstration 4 Output Demonstration 5 Output Lambda Function Demonstration 6 Output Demonstration 7 Output Modules Demonstration 8 Output Using Alias Demonstration 9 Output Demonstration 10 Output Quick Discussion on pip Solution to Exercises CS 5.1 Implementation Possible Improvements CS 5.2 Implementation Possible Improvements Case Study VI Chapter 8: Exception Management Types of Mistakes General Philosophy Common terms Python Exceptions Demonstration 1 Case Study with Valid Inputs Case Study with an Invalid Input Case Study with another Invalid Input Key Points of the Exception-handling Mechanism Demonstration 2 Case Study with Valid Inputs Case Study with an Invalid Input Case Study with another Invalid Input Demonstration 3 Case Study with Valid Inputs Case Study with Invalid Inputs Use of pass Statement Demonstration 4 Case Study with Valid Inputs Case Study with an Invalid Input Analysis Arranging Multiple except Blocks Case Study with an Invalid Input Case Study with another Invalid Input Custom Exception Demonstration 5 Output Debugging Code Using assert Statement Demonstration 6 Output Demonstration 7 Output Analysis Solution to Exercises CS 6.1 Implementation Case Study VII Chapter 9: Programming with Files Reading from a File Quick Talk about File Paths Demonstration 1 Output Analysis Writing to a File Demonstration 2 Output Analysis Alternative Way to Read and Write Demonstration 3 Output Analysis Working with Binary Files Demonstration 4 Output Renaming and Removing Files Demonstration 5 Output Using with Keyword Demonstration 6 Output for Approach1 Output for Approach2 Output for Approach3 Handling FileNotFoundError Demonstration 7 Output Analysis Solution to Exercises CS 7.1 Implementation Chapter 10: Object-Oriented Programming Concepts Class and Objects Encapsulation Abstraction Inheritance Polymorphism Q&A Session Case Study VIII Chapter 11: Class, Objects, and Inheritance Constructing the Building Blocks for OOP Demonstration 1 Output Analysis Constructor Demonstration 2 Output Analysis Looking into Instance Creation Process Demonstration 3 Output Analysis Changing an Attribute Value Demonstration 4 Output Working with Default Attributes Demonstration 5 Output Analysis Importing Classes Demonstration 6 Output Analysis Demonstration 7 Output Demonstration 8 Demonstration 9 Output Use of Identity Operator Demonstration 10 Output Inheritance Demonstration 11 Output Analysis Updated Implementation Output Demonstration 12 Output Private Variables and Methods Demonstration 13 Output Analysis Static Methods and Class Methods Understanding the Difference Demonstration 14 Output Analysis Solution to Exercises CS 8.1 Implementation Possible improvements Chapter 12: Test Your Code The Code for Testing Testing Functions Demonstration 1 Output Demonstration 2 Output Analysis Testing Class Demonstration 3 Output Solution to Exercises Index About the Author Vaskaran Sarcar obtained his Master of Engineering in Software Engineering from Jadavpur University, Kolkata (India), and an MCA from Vidyasagar University, Midnapore (India). He was a National Gate Scholar (2007-2009) and has over 12 years of experience in Education and the IT industry. He devoted his early years (2005-2007) to teach at various engineering colleges, and later he joined HP India PPS R&D Hub Bangalore. He worked there until August 2019. At the time of his retirement from the IT industry, he was a Senior Software Engineer and Team Lead at HP. To follow his dream and passion, Vaskaran is now an independent full-time author. Other books by him include: Design Patterns in C# Second Edition (Apress,2020) Getting Started with Advanced C# (Apress,2020) Interactive Object-Oriented Programming in Java Second Edition (Apress,2019) Java Design Patterns Second Edition (Apress,2019) Design Patterns in C# (Apress,2018) Interactive C# (Apress,2017) Interactive Object-Oriented Programming in Java (Apress,2016) Java Design Patterns (Apress,2016) C# Basics: Test Your Skills (Createspace,2015) Operating System: Computer Science Interview Series (Createspace,2014) Acknowledgments At first, I thank the Almighty. I sincerely believe that with HIS blessings only, I could complete this book. I extend my deepest gratitude to all my family members, my publisher, the editorial board members, and everyone who directly or indirectly supported this book. Preface Welcome to your journey through Python Bookcamp: Exercises and Hand-on Projects. This is an introductory guide to the Python programming. Before you jump into the topics, I want to highlight a few points about the goal and the organization of the book. The primary goal of this book is to make you familiar with Python programming as quickly as possible. Once you learn the concepts in this book, you’ll be ready to explore further. I have been involved in teaching since 2005. I have taken classes at both engineering and nonengineering colleges. This is the true motivation to introduce a book like this. The book helps you to learn the fundamental concepts effectively. What is an effective way? Ok, once you learn a topic, if you repeatedly practice and try to write code, you are on the right track. This is why this book has multiple exercises and hands-on projects. The book follows a steep learning curve, where you keep implementing these case studies using the concepts you learn in a previous chapter. Starting from the second chapter, each chapter in this book contains exercises too. These exercises are neither too easy nor too tough. These are within your optimal zone of difficulty. As per Goldilocks rule, you can motivate yourself and act better in these situations. The exercises and case studies help you test your understanding and raise your confidence level. Many of us are afraid of fat books because they do not promise that we can learn the subject in one day or 7 days. But you know that learning is a continuous process. It is hard to achieve any real mastery in 24 hours or 7 days. So, the motto of the book is “To learn the core topics of Python, whatever effort I need to put, I am ok with that.” Still, simple arithmetic says that if you can complete one topic in one day, you can complete the book within 12 days (your learning speed depends only on your concentration level, focus, and dedication). But this arithmetic calculation is secondary! I have designed the book in such a way that upon completion of the book, you will know the core concepts in Python. Most importantly, you’ll know how to learn further. Python is a very popular computer language and widely used. Like other popular programming languages, it grows continuously to give us support with additional features and functionalities. When I started writing this book, Python3.8 was the latest version. So, everything in this book should run in Python 3.8 and upcoming versions. I choose this version for another reason. The official website tells us that Python 3.x (Commonly known as Python 3) is the future of the language, but Python 2. x (Also known as Python 2) is the legacy. You may see some old Python projects with Python2, but I recommend you to learn and use Python3. Once you are familiar with a good programming language, you find many similarities with other programming languages. As an obvious result, you can motivate yourself to learn a new programming language and this time it will be even easier to grasp. How the book is organized? The book has the following organization: The first chapter is a warm-up session for you. Here you’ll set up your programming environment and make yourself ready for further learning. In the Chapter 2, you’ll learn about comments, variables, and operators. I told you that this book includes multiple exercises and projects. An implementor must know what he/she is going to implement. So, at the beginning of each subsequent chapter, you will get an overview and description of the project that can be implemented. Once you finish reading the chapter, you’ll get the complete implementation of the projects. In Chapter 3, you’ll learn about common data types, such as strings and numbers in Python. In Chapter 4, you’ll learn about decision making in your program. Chapter 5 talks about iteration. Here you’ll learn about loops and the usage of break and continue statements. In Chapter 6, you’ll learn advanced data types, such as lists, tuples, sets, and dictionaries. Chapter 7 teaches you how to use functions to make your code more Pythonic. You’ll also learn about modules and their usage. Chapter 8 talks about exceptions and how to manage them. Chapter 9 teaches you file handling mechanisms. Chapter 10 and Chapter 11 of this book briefly cover the object-oriented programming basics and show you the usage of classes, objects, and inheritances. Here you also learn about static methods, class methods, and private variables, etc. At the end of the book, there is a chapter (Chapter 12) to show how to write useful tests to verify your code. You can download all the source codes of the book from GitHub (I already gave you the online link). I have a plan to maintain the “Errata” and if required, I can also make some announcements there. So, I suggest that you visit those pages to receive any important corrections or updates. Prerequisite Knowledge The target readers for this book are those who are new to Python programming. The book will be super easy for the readers with the least coding experience in any other high-level computer language. I assume that you can download a software installer following the online instructions and you already installed Python on your computer. So, I do not spend time on this topic. It is because you can find them easily both online and offline. Who is this book for? In short, you can pick the book if the answer is “yes” to the following questions: Have you never programmed before, but eager to learn Python? Do you have experience with one or more high-level programming languages, but want to learn Python this time? Do you want to explore the Python basics step-by-step, but as quickly as possible? Do you want to be familiar with object-oriented concepts like polymorphism, inheritance, abstraction, and encapsulation? Do you know how to install software on a machine and then set up the coding environment? Do you like to review your knowledge before you use Python in advanced fields such as data science, machine learning? Probably you shouldn’t read this book if the answer is yes to any of the following questions: Are you confident about the fundamentals of Python? Are you looking for advanced concepts in Python, excluding the topics mentioned previously? Do you dislike a book that has an emphasis on exercises? “I dislike Windows OS, and PyCharm. I want to learn and use Python without them only.”-Is this statement true for you? Guidelines for Using This Book Here are some suggestions so you can use the book more effectively: I suggest you reading these chapters sequentially. The reason is that some fundamental techniques/concepts may be discussed in a previous chapter, and I do not repeat the same in a subsequent chapter. I also suggest that you complete the exercises in a chapter before you enter a new chapter. This process can give you confidence, which can give you a better pay-off soon. Indentation is an important part of Python programming. We consider anything indented as a block of code in Python. Based on your device’s screen size, you may not see the correct indentation in some programs. I suggest that you refer to the actual code in those cases. You can download the actual code from the online link https://github.com/Vaskaran/PythonBookcamp. I mentioned this link at the beginning of this book. It is worth mentioning that I could use IDLE, which is Python’s Integrated Development and Learning Environment. It provides some basic functionalities such as syntax highlighting, different uses of colors for better readability. But I have used Python Command Shell (for the initial chapters where lines of code are very short) and PyCharm Community Edition 2020.1.1 in the Windows 10 environment. PyCharm has many interesting features and when you work with large projects, those features are very useful. The Community edition of PyCharm is also free of cost. If you do not use the Windows operating system, you can also use Visual Studio Code, which is also a source-code editor developed by Microsoft to support Windows, Linux, or Mac operating systems. This multi-platform IDE is also free. In some cases, I have shortened the long URLs for a better presentation. For example, you’ll see http://bit.ly/python-exceptions instead of https://docs.python.org/3/library/exceptions.html#:~:text=In%20Python,%20 In this book, I have written the programs are written and explained the concepts in a way that these different programming environments should not cause any major problem to your learning process. I have used Python 3.8 for this book, which was the latest version available at this time of writing. You can surely predict that version updates will come continuously, but I strongly believe that these version details should not matter much to you because I have used the fundamental constructs of Python. So, these codes should execute smoothly in the upcoming versions of Python/PyCharm as well. I also believe that the results should not vary in other environments, but you know the nature of a software-it is naughty. So, if you like to see the same output, it is better to mimic the same environment. Remember that you have just started on this journey. As you learn about these concepts, try to write your code. It helps you write better programs. Conventions in This Book Here I mention only two points: In many places, to avoid less typing, I have used the word “his” only. Please treat it as “his” or “her” -which applies to you. Second, all the outputs and codes of the book follow the same font and structure. To draw your attention, in some places, I have made them bold. For example, consider the following code fragment (I’ve taken it from Chapter 8) and the lines in bold. # Previous codes are skipped except ZeroDivisionError as e: print("Invalid input! Your divisor becomes zero!") print(f"Error details:{e}") except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details:{e}") else: print(f"Result of the division is : {result}") print("The program completes successfully.") Finally, I hope that this book can provide help to you and you will value the effort. Chapter 1: Getting Ready for Program Execution This chapter briefly talks about the Python language and its importance. Here you’ll learn how to set up your programming environment before you execute the programs. Overview Python is a computer programming language that is rapidly growing nowadays. The primary reasons for its popularity are simplicity and readability. Guido van Rossum created this in the late 1980s. You can use Python for various purposes. For example, you may notice its usage in game programming, business applications, tools development, etc. Using Python programs, you can automate boring and lengthy tasks to save your time and energy. In recent years, you notice it in emerging fields like data science and machine learning too. Last but not least is that the Python community is very strong and supportive and they help you grow faster. Setting Up the Programming Environment Python is a high-level language. So, you can avoid direct interaction with registers, memory addresses, call stacks, etc. Instead, you write your program in plain English. So, how the computer will understand the instructions? Well, you may need to be familiar with two terms-compilers and interpreters. I’ll cover them later. Now you can keep in mind the simple distinction between these two: An interpreter translates one instruction (or statement) at a time into machine code. But a compiler takes the entire program and then translates it into machine language in one shot. For now, you remember that you need a Python interpreter to interpret these programs. So, you need to get the interpreter and install it before you write your programs. You need to pick the correct interpreter based on your operating system. A popular programming language grows continuously to give us support for more features and functionalities. When I started writing the book, Python3.8 was the latest version. So, everything in this book should run in Python 3.8 and upcoming versions. I choose this version for another reason. The official website tells us that Python3.x (known as Python 3) is the future of the language, but Python2.x (known as Python 2) is the legacy. So, you may see some old Python projects with Python2, but I recommend you to learn and use Python3. Using Command Prompt You can use a text editor to write a Python program. For example, you can use a notepad. But before you do this, let us begin with a simple command prompt to check whether Python is installed in your system. So, open the command prompt and type python (See Figure 1-1): Figure 1-1: Checking whether Python is installed in the system. I told you earlier that I assume Python is already installed on your computer. If Python is installed on your computer, you notice the detailed information as shown in Figure 1-1. Now you can use some simple commands for further verification purposes. Each time you enter a command and press the Enter (or Return) key, the statement will be tested. Here are some examples: C:\Users\Vaskaran Sarcar>python Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> 2+3 5 >>> 5>3 True >>> 2<1 False >>> print("Hi") Hi >>> You can type exit() (Or, or Ctrl-Z plus Return ) to quit from this shell. Using IDLE You can also launch IDLE to get a python shell where you can execute python commands. For example, to get IDLE in Windows10, you can type IDLE in the search box as shown in Figure 1-2: What is a shell? In simple terms, it is an environment that is used to run other programs. We can use shells for both the commandline interface and graphical user interface (GUI). But normally we use it to refer to the command-line interface of the operating system (OS). Developers often call the terms-‘shell’ and ‘terminals’ in the same context interchangeably. Figure 1-2: Searching IDLE Once you can see the app, click on it to launch. Now you can see the shell. Figure 1-3: Python 3.8.3 is launched. This shell waits to get a particular command from the user, executes the command, and then displays the result. Once this cycle is completed, it waits for the next command to receive from the user. Let us try this. Type print("Hello World!") after >>> in the shell, and press the Enter key. You can immediately see the output Hello world in the next line as shown in the following figure. Figure 1-4: Printing ‘Hello World’ using IDLE. Now type 1+2 in the shell and press enter to get the following output: >>> 1+2 3 Now you may try a few more lines of code, and verify the output like the following: >>> 10>7 True >>> 2+7<5 False Hopefully, you get an idea- how to write some simple programs in a Python shell. To execute some basic commands, or to check whether you are ready for python programming, this process is fine. But the problem is: once you exit from the shell, we lose all these commands. This is why we can use a text file to write a Python program and save the file with the .py extension. In short, the file with the .py extension is called a python script. Using PyCharm IDE I have shown you the use of simple command prompts to make you aware of the alternative ways to run your Python programs. But ask any professional about how they write programs. You’ll come to know that they use specialized text editors or IDE’s (IDE stands for Integrated Development Environment) to write the code. It is because the use of command prompts is not suitable for big programs. IDE’s help you highlight the syntax error and they can provide you automatic suggestions to write error-free code. They also support auto-completion for certain functions and phrases, which are extremely helpful. There are many IDE’s with cross-platform support too. But in the end, a few of them became popular, and widely used for a particular computer language. For example, Java developers often use Eclipse when they write programs; similarly, Visual Studio is very common for .NET developers. The same concept applies here. For Python programming, there are many IDE’s, and PyCharm is very common. I have used this IDE to develop my programs (aka python scripts) for this book. Spyder is another open-source and cross-platform IDE, which we often in Python programming. For my machine learning projects, I use Jupyter Notebook in which you can test Python scripts too. If you install Anaconda distribution on your computer, you can find both Spyder IDE, Jupyter Notebook, and many other things. But I believe that to learn and test simple Python scripts, PyCharm is a pleasant choice for you. It helps you to organize your files, identify syntax errors. You can also set breakpoints to pause at specified lines in your program. You find code refactoring very easy when you use PyCharm. For the following program, I took some screenshots from the PyCharm IDE. These can help you visualize the execution environment. But next time onwards, I’ll show you the programs and corresponding output only. As said before, to execute the python scripts, PyCharm is NOT mandatory. You can run these programs in various ways (for example using IDLE, Spyder IDE, Jupyter Notebook, etc). So, it makes little sense to take screenshots from PyCharm for each of these programs. Let us follow these steps: Step-1: Open PyCharm Step-2: Click on File > New Project Figure 1-5: Creating a project in PyCharm. If you have multiple Python versions installed on your computer, you can see them in the drop-down list in the previous screenshot (Figure 1-5) and you can pick your preferred python version from here. I am using the latest version (3.8) which is available at the time of this writing. Since I have already executed some python scripts using PyCharm, I get this option. But for the first time users, you do not see this window. Figure 1-6: Project open options in PyCharm. Step-2.1: I opt for a new Window. Now I get the following screen. See Figure 1-7. Figure 1-7: The project is opened in a new window. Step-3: Right-click on the project folder name (PythonCrashCourse). Select “New” and then choose “Python File” ( See Figure 1-8) Figure 1-8: Adding a new Python file. Step-3.1: Name it as hello_world. See Figure 1-9. Figure 1-9:Name the file as hello_world.py Step-3.2: Press Enter. Notice that a new python file is created for you. See Figure 1-10. Figure 1-10: The hello_world.py is ready. You can write the code here. Now you are ready to write your first program in PyCharm. Executing Simple Programs Now I show you some simple Python programs using PyCharm IDE. Demonstration 1 Let us write a simple python program. Type the following line: print("Hello World!") in a python file (Refer to the following screen). Now move the cursor to the next line by pressing the “Enter” key as shown in the following figure. See Figure 1-11. (I have organized the code of this book chapter-wise. So, in this screenshot, you see other files and directories too.) Figure 1-11: The hello_world.py file contains a line of code. Save the file (File >Save All (Ctrl+S)). Now you can run the program. Right-click on the file name and press the option Run ‘hello_world’ as shown in the following figure. See Figure 1-12. Figure 1-12: Run hello_world.py in PyCharm Output Congratulation! You have successfully executed your first python program in PyCharm IDE. You can see the following output: Hello World! Now I take a snapshot from PyCharm to depict it better. See Figure 1-13. Figure 1-13: The successful execution of the program generates the output. If you want to run the program again, you can use the following button as shown in the following figure. Notice that ‘ hello_world’ is selected by default before you press the run button. See Figure 1-14. Figure 1-14: Alternative option to run hello_world.py POINTS TO REMEMBER Every time you write a new program, you can follow the same approach. Next time onwards, I’ll show you the programs and output only. It is because you can run the python scripts in various ways(for example, you can use a Python shell, you may opt for IDLE, etc). So, it makes little sense to take screenshots from PyCharm in each case. Following the PEP8 guideline (You can refer to the link https://www.python.org/dev/peps/pep0008/), I have named my file name hello_world.py. This guideline suggests that you choose all lowercase names for your modules. But you can use underscore to improve readability. I’ll discuss modules later in Chapter 7. For now, you know that a module is a python file with a .py extension. Let us have some fun with python scripting and continue to write another simple program. In Demonstration2, I am using asterisks (*) to make a shape which makes a triangle shape. Demonstration 2 Create a new python file. Let us call it triangle.py and type the following lines into it. print("*") print("**") print("***") print("****") print("*****") Now save the file and run the program. Output Here is the output. * ** *** **** ***** ****** Let us go through another simple program in which I’m calculating the sum of two numbers. Demonstration 3 Create a new python file. Let us call it sum.py and type the following lines into it. print("The sum of 12 and 5.7 are as follows:") print(12+5.7) Output Here is the output. The sum of 12 and 5.7 are as follows: 17.7 I know that this chapter was a bit slow, but it is important. These brief discussions will help you understand the theory and do the code exercises in the upcoming chapters. Chapter 2: Comments, Variables, and Operators In this chapter, you see some building blocks to develop your Python programs. First, you’ll learn how to write comments and use variables in a program. Later you’ll learn how to use operators in your program. Using Comments It is a standard practice to use comments in your program. These comments can help others to understand your code better. Let us consider a real-life scenario. In a software organization, a group of people creates software for its customers. It is possible that after some years, none of them are available. Either these members move into a different team, or they leave the organization. In such a case, someone needs to maintain the software and continue fixing the bugs for its customers. But it is very difficult to understand the logic if there is no hint or explanation about the program logic. Comments are useful in such scenarios. These help us understand the code better. In Python, you see the following options for comments: Case-1: Single line comments using # tags. Here is an example: # This is a single-line comment Case-2: Multi-line docstrings as multi-line comments using three double quotations (or single quotations). You can see one-line docstrings as well. Here I show you the use of docstrings that I use inside a function in Chapter 7. For now, you do not need to understand the code. You only see the texts that begin with triple-double quotations (" " ") and ends with tripledouble quotations (" " ") def print_details(name,age): """ This function takes two parameters. You can supply the name and age of the user in this function. """ print(f"Hello {name}!How are you?") print(f"You are now {age}.") POINTS TO REMEMBER Comments are simple notes or some texts. You use them for human readers, but not for the Python interpreter. Python interpreter ignores the text inside a comment block. In the software industry, many technical reviewers review your code. The comments help them understand the program logic. A developer can forget the logic after some months. These comments can help him recollect his logic. Experts prefer to use the many single-line comments using # tags. In Python programming, functions, modules, and classes should have docstrings. These will make sense when you see them in Chapter 7 or Chapter 11. You’ll know that a docstring becomes the _doc_ attribute of an object. I leave the discussion at this point. In Chapter 7 and Chapter 11, you’ll see that I use docstrings to describe a function and class behavior. In this chapter, you learn the use of simple comments that you may see in other’s code. Demonstration 1 When the Python interpreter sees a comment, it ignores that. To understand this, follow the next program. I repeat that experts suggest that we use singleline comments with # tags. Use of the docstrings is common when you use a function, module, or class. You’ll be familiar with them later. # Testing whether 2 is greater than 1 print(2>1) ''' I’m using these as multi-line comments. I use these lines for your reference only. These are common in class, functions, or modules. ''' """ I’m trying to use multi-line comments using three double-quotes. I use these lines for your reference only. These are common in class, functions, or modules. """ # Now I'm showing multiple single-line comments # Multiplying 2 with 3 # And printing the result print(2*3) Output Here is the output. True 6 Analysis This program uses different comments, and it is easy to understand. You can see that you have received output for the lines print(2>1) and print(2*3) only. The remaining portions(comments) are ignored by the interpreter. Since 2 is bigger than 1, so the output came as True , and when you multiply 2 with 3, the result is 6 . Introduction to Variables In Mathematical Algebra, you often write something like x=10. Then you say x is a variable to represent the number 10 . Python works in the same way, except here you can represent both numeric and non-numeric values using variables. In the following sections, for your easy understanding, I use some string and number variables. You’ll learn more about them in the next chapter (Chapter 3). To continue this discussion, I quickly cover two important points: What do I mean by the words- strings and numbers? What is the difference between int and float ? Strings vs Numbers In simple words, strings are texts. They are very common in Python programming. You have already seen the usage of strings in earlier demonstrations. For example, to print Hello World! I wrote the following: print("Hello World!") I can also use a string variable like the following: my_text = "Hello World!" and then print the output using the following line of code: print(my_text) Notice that in both cases, I enclose Hello World! inside the double quotation marks. This is the general format to declare a string variable. You can use single quotations too. Both are fine. Sometimes you may need to use both intelligently. For example, the following code works fine: >>> print("This is Sam's book.") This is Sam's book. But if you use single quotations like the following, you’ll get errors. Here is a sample: >>> print('This is Sam's book.') File "<stdin>", line 1 print('This is Sam's book.') ^ SyntaxError: invalid syntax You can use an escape character to avoid the error. Here is a sample: >>> print('This is Sam\'s book.') This is Sam's book. You’ll learn more about escape characters in Chapter 3. POINT TO REMEMBER You can use single quotations or double quotations to print the output messages. Both are fine. Python does not distinguish between single and double quotation marks. You need to remember the simple fact: you enclose a string literal in matching single quotation or double quotation marks. In most cases, you see me using double quotation marks. It is because I learned Java, C# earlier, and naturally double quotations come in my coding. I have seen people using single quotations to print simple messages and double quotations for formatted strings. But note that this is only a preference, but not a convention. The following link: http://bit.ly/3i8zWAw says: “In plain English: Both types of literals can be enclosed in matching single quotes (') or double quotes ("). They can also be enclosed in matching groups of three single or double quotes (these are generally referred to as triple-quoted strings).” Refer to the official documentation http://bit.ly/python-official-documentation whenever you have similar doubts. Numbers are also heavily used in Python programming. Like strings, to print a number without a variable, you can just type the number inside the print() function as follows. I keep the inline comments to show what these lines can print for you. print(1)#Prints 1 print(5.7)# Prints 5.7 print(-6.789)# Prints -6.789 You can also use number variables like the following: # Using variables my_int=125 print(my_int)# Prints 125 my_float=25.763 print(my_float)# Prints 25.763 I want you to notice that I’ve NOT enclosed these numbers inside double quotations. This is the general format to declare a number variable. This slight difference in a declaration can make a big difference in output. For example, consider the following code segment, and notice the inline comments to know the output: # Difference between the numbers and the strings print("1"+ "2") #Prints 12 print(1+2) #Prints 3 Now I want you to note another key distinction. This time both are numbers, but they are categorized as int and float . Let us have a look. int vs float In Python, integers are whole numbers; they do not have fractional parts. An integer can be positive, negative, or 0. For example, 23,-32,67,-2,0,25 are examples of integers. In Python, we refer to an integer as an int . In contrast, 35.75, 45.2, etc. are not integers because they have fractional parts. These are floating-point numbers. In Python, you simply call them the float datatype . Now you are ready to proceed further. Before I discuss more on Python variables, let me show you a program. This program can make more sense to you when you go through the analysis followed by the output of the program. Demonstration 2 Now I change the Demonstration1 in the previous chapter (Chapter 1). Here I add a few more lines of code into the program. I create a new python file and name it hello_world_modified.py. Then I type the following lines into it. print("*** This program shows the use of a variable. ***") # mytext is holding the value "Hello World!" my_text = "Hello World!" print(my_text) # my_text is holding a new value my_text = "Dear Reader, how are you?" print(my_text) Output Here is the output. *** This program shows the use of a variable. *** Hello World! Dear Reader, how are you? Analysis This output shows you an alternative way to print “Hello World!” . So, what was the key modification? Instead of writing the following line: print("Hello World!") Now I have written the following lines: my_text = "Hello World!" print(my_text) You can see that in both cases, I have received the same output. But why should I write more? One possible answer can be found when you notice the last part of the program which is as follows: my_text = "Dear Reader, how are you?" print(my_text) You can see that you can store different data inside my_text and manipulate them later. Most importantly, you can change the value later. It is the key use of a variable. You got it right; my_text is an example of a variable that you have seen in this demonstration. In this example, you can notice that the initial value ( Hello World ! ) of my_text is changed later in this program. To explain the program better, I can use comments in this program. You learned about comments already, and here you see another valid use case of them. Now I summarize the key points: Using an assignment operator (=) you can assign a value to a variable. You can assign a variable and reassign it again. You do this when it makes sense to you. Note that the type of variable change if you reassign an expression with a different type. Let us test this in a Python shell: >>> number1=2 >>> number2=3 >>> print(number1+number2) 5 >>> number1=2.0 >>> print(number1+number2) 5.0 You can see that when number1 was reassigned to the value 2.0, the resultant sum of number1+number2 becomes 5.0 instead of 5 . Alternatively, you can test the following block of code. Notice that if you assign 2 to the variable number1 , the Python interpreter can determine that it is an int . But when you assign 2.0 to the variable number1 , it can recognize it as a float data type. >>> number1=2 >>> type(number1) <class 'int'> >>> number1=2.0 >>> type(number1) <class 'float'> Naming Conventions You can remember the following points when you use variables in your program. You use an assignment operator (for example, =) to assign a value to a variable. You’ll see different assignment operators shortly. In real-world applications, you use a meaningful and descriptive name for your variables. Try to avoid one-character variable names except inside some loops or functions. You will know about loops and functions and use them in later chapters. For now, you can note this point. In simple demonstrations to type less, sometimes I ignore this convention. So, in some code fragments, you may see something like x=5 . But in a real-world application, choose a better name, which is meaningful enough. For example, to assign an identification number (say, 5 ) to an employee in an organization, you can write something like employee_id=5 Your variable name should not clash with Python keywords. A keyword is a reserved word, you cannot use them as ordinary identifiers. Python 3.9 has released just now. You can refer to the online link https://docs.python.org/3/reference/lexical_analysis.html#identifiers to know about the latest keywords. For your immediate reference, I include them here: False await else import pass None break except in raise True class finally is return and continue for lambda try as def from nonlocal while assert del global not with async elif if or yield So, if you write for=25, you’ll get an error. Here is a sample for you: >>> for=25 File "<stdin>", line 1 for=25 ^ SyntaxError: invalid syntax >>> for_variable=25 >>> for_variable 25 Like a different computer language, Python has many in-built functions. Your variable name should not clash with those names. For example, in Python, you can use the abs() function to calculate the absolute value. Here are some examples. >>> abs(23.7) 23.7 >>> abs(-5.7) 5.7 >>> abs(3-7.5) 4.5 So, ideally, you should not use something like abs=45.2. In this context, there is another interesting point to note. Let us say, by mistake, you use abs=45.2 earlier in your code. Now if you call abs(-10) , a Python shell can show you an error. Here I show you such a sample. >>> abs=45.2 >>> print(abs) 45.2 >>> abs(-10) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'float' object is not callable A similar error may appear if you write something like abs=45 and then you try to call abs(-10) . Here is a code fragment: >>> abs=45 >>> print(abs) 45 >>> abs(-10) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable Both the errors are similar. The only difference is that in the first case, it complains about the float , but in the next case, it says about an int . It is obvious because you know that 45.2 is a floating-point number, but 45 is an integer. So, you understand that this kind of error tries to say that you have used abs before this call. A simple remedy to this is to exit from the shell (or remove the erroneous variable naming practice) and try to use the inbuilt function again. Here is a code fragment from the Python shell for your reference. C:\Users\Vaskaran Sarcar>python Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> abs=7 >>> print(abs) 7 >>> abs(-5.7) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable >>> exit() C:\Users\Vaskaran Sarcar>python Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> abs(-5.7) 5.7 You can see that when I see the following error: (TypeError: 'int' object is not callable) I get a clue that before this line somewhere I used abs as an Integer. So, I exit from the shell and re-enter the shell again. This activity deletes old assignments. This time I do not perform the illegal variable naming practice too. So, the in-built function abs() again work fine for me. You can use any of the following naming conventions and you’ll not receive any error for any of these. For example, to name a student, you can use your variable name as: studentName (Lower camel case) StudentName (Upper camel case, also known as Pascal case) student_name (Snake style, words are separated by an underscore) Student_name (Also a snake style, but first word starts with a capital letter) Variable names can be alphanumerical, but the first letter must start with a letter. For example, student_1 , student1 both are fine, but 1student or 1_student will raise an error. Here is an example for your reference. >>> student_1="John" >>> print(student_1) John >>> student2="Sam" >>> print(student2) Sam >>> 3student="Kate" File "<stdin>", line 1 3student="Kate" ^ SyntaxError: invalid syntax You have seen that you can use an underscore in your variable name, but other special characters can raise an error in your program. For example, if you use @ inside your variable name, you’ll receive an error. Here is an example. >>> student@name="Jack" File "<stdin>", line 1 SyntaxError: cannot assign to operator The variable name should not start or end with an underscore. Though you do not see any error for that, experts strongly discourage this practice. For example, you should not use something like: _student = ”John” or student_= ”John” If you ask me: Among these accepted naming conventions, which one is my favorite? I’ll answer that I like to use the snake style, which is something like the following: student_name=” Jack” I recommend the same practice for you. Look at the following line again: my_text = "Hello World!". You can see I use the assignment operator ( = ). The variable ( my_text ) is placed at the left-hand side of the assignment operator, and the intended value is placed on the right-hand side of the assignment operator. So, you can say that using the previous line of code, I assign the value Hello World! to the variable my_text . Similarly, if I write x=25 , I say that I assign 25 to x . So, you never see the code like: 25=x #This is an error If by mistake, you do this, you’ll receive the syntax error. For your reference, I show the error from a Python shell: >>> 25=x File "<stdin>", line 1 SyntaxError: cannot assign to literal If you are coding for the first time, you may be confused about the usage of the assignment operator. Let me explain. From a mathematical point of view, it may appear to you that both these statements: x=y and y=x can have the same meaning, but in programming, it is different. You need to keep the information from previous paragraphs in your mind. To make it clear, you can make a small test using the following code segment in the Python shell: >>> x=25 >>> y=50 >>> print(x) 25 >>> print(y) 50 >>> x=y >>> print(x) 50 You can see that initially, x has the value of 25. But once you use the line of code: x=y The current value of y is assigned to x . So, when you print the value of x , you get 50 . Conversely, if you use the following line of code: y=x The current value of x will be assigned to y . Let us test this too. >>> x=25 >>> y=50 >>> y=x >>> print(x) 25 >>> print(y) 25 I hope that you have got the idea! Assigning Multiple Variables in a Single Line In Python programming, you can assign multiple variables in one statement. Let us have a look into the following segment: >>> x,y,z = 10,25,-303.5 >>> print(x) 10 >>> print(y) 25 >>> print(z) -303.5 Here you can see that when you use the line of code: x,y,z = 10,25,-303.5 The values 10, 25 , and 303.5 are assigned to x, y, and z respectively. A tuple is a comma-separated list of expressions. So, you can say that in the previous example, x, y, and z form one tuple, and 10,25 and -303.5 form another tuple. We refer to this assignment process as a tuple assignment. You will learn more about tuples in detail in Chapter 6 Assigning Same Value to Multiple Variables Now let us explore another interesting feature in Python programming. You can assign the same values to multiple variables in one statement. For example, in the following code snippet, I am assigning all the three variables ( x,y, and z ) the same value ( 100 ). >>> x=y=z=100 >>> print(x) 100 >>> print(y) 100 >>> print(z) 100 Operators Operators are special symbols, which are used to carry out some kind of assignment or computations. These operators work on some values, which are termed operands. For example, in the expression: 2+3 , + is an operator, and 2,3 are the operands. In simple terms: a literal value, say 5 , or a variable, say empId are examples of simple expressions. You can combine values, variables, and operators, etc. to make a complex expression. We can check an expression to get a value at the end. You have seen how to assign value to a variable using an assignment operator (=). Python supports many other operators which can be classified as follows: Arithmetic operators Assignment operators Comparison operators Logical operators Bitwise operators Identity operators Membership operators Let us test some common operators. For your immediate reference, let us go through the following examples. These are very easy to understand. I’m executing them in the Python shell. Arithmetic Operators These are used to perform common mathematical operations. Before I use the arithmetic operators, I use two variables, x, and y. I assign them with the initial values 25 and 10 as follows: >>> x=25 >>> y=10 Addition operator (+): >>> print(x+y) 35 Subtraction operator (-): >>> print(x-y) 15 Multiplication operator (*): >>> print(x*y) 250 Division operator (/): >>> print(x/y) 2.5 Modulus (Or, Remainder) operator (%) >>> print(x%y) 5 Explanation: If you divide 25 by 10, 5 is the remainder. Exponentiation operator (**) >>> print(y**3) 1000 Explanation: 10*10*10=1000 Floor Division (//) operator: >>> print(x//y) 2 Explanation: You get the answer to the nearest whole number. Consider another example: 11/3=3.666(approx); so, 11//3 gives you the answer as 3. POINTS TO NOTE The interactive Python shell can test both expressions and statements. For example, if you type 10, the shell can interpret it as 10. See the following segment: >>> 10 10 But when you enter x=10 in the Python shell, it is treated as a correct syntactical statement with no value, and the shell prints nothing. In the next line, when you enter x, the shell can test the value of x. As a result, it can print the value of x now. This is is why it is interesting to note that in Python shell, when I print the values, instead of typing print(x), you can simply type x to get the value of x. For example, see the following code segment. >>> x=10 >>> x 10 But, by mistake, if you try to print the value of another variable, say, y which has not been defined already, you’ll encounter errors. Here is such a segment for your easy reference: >>> y Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'y' is not defined For the upcoming examples in this chapter, I use this shorthand form to print the value of a variable in the Python shell. Assignment Operators You can use these operators to assign some values to the variables. You can use them for less typing. For example, x+=2 is shorthand for x = x+2 . Similarly, x -= is a shorthand for x = x-2 . Let us test some of them. Before I show them in the following examples, in each case, I assign an arbitrary value 5 to x as follows: >>> x=5 Using +=: >>> x=5 >>> x+=2 >>> x 7 Using -=: >>> x=5 >>> x-=3 >>> x 2 Using *=: >>> x=5 >>> x*=2 >>> x 10 Using //=: >>> x=5 >>> x//=2 >>> x 2 Similarly, you can see other assignment operators like **=, /=, %=, etc. Comparison (or Relational) Operators These operators are used to compare the equalities (or, inequalities) of two values (or, operands). The result of the comparison is a boolean value, i.e., either True or False . You can use Table 2-1 for your reference. Table 2-1 The relational operators Here I test some statements using the comparison operators in a Python shell. >>> 5>2 True >>> 5<2 False >>> 20==15+5 True # Assigning 10 to x, 15 to y and # then I’m using comparison operators. >>> x = 10 >>> y = 15 >>> x == y False >>> x != y True >>> x == y-5 True >>> x >= y False >>> x <= y-2 True Logical Operators You can use these operators when you work on conditional statements. Here you see the uses of “logical and”,” logical or”, and “logical not” operators. To understand these, let us suppose you are testing two statements. For a Logical OR, if at least one statement is true, the combined result is True, otherwise False. You use ‘or’ to denote logical OR. For a Logical AND, if both statements are true, the combined result is True, otherwise False. You use ‘and’ to denote logical AND. A logical NOT does the opposite. It reverses the result. For example, if the result is True, it reverses it to False and vice versa. You use ‘not’ to denote logical NOT. For an easy understanding, I show you some examples in a Python shell. >>> 5>3 and 3>1 True >>> 5>3 and 2>4 False >>> 5>3 or 2>4 True >>> not(5>3) False >>> not(2>3) True >>> not(5>3 or 2>4) False >>> x=10 >>> y=15 >>> x>=10 and y<5 False >>> x>5 and y>7 True Bitwise Operators We use these operators for binary numbers. Here operands are integers, but Python treats them as binary digits. Here we compare bit-by-bit of the binary codes. This is why we call them bitwise operators. The common bitwise operators are: Bitwise OR: You use ‘|’ to denote bitwise OR. When you compare two bits, the resultant bit is 1, if at least one of the two bits is 1. Bitwise AND: You use ‘&’ to denote bitwise AND. When you compare two bits, the resultant bit is 1, if both bits are 1. Bitwise XOR: You use ‘^’ to denote bitwise XOR. When you compare two bits, the resultant bit is 1, if ONLY one of the two bits is 1. Bitwise NOT: You use ‘ ~ ’ to denote bitwise NOT. This operator inverts all the bits in a binary number (i.e, you change 1 to 0 and 0 to 1 for each bit) Zero fill left shift: You use ‘<<’ to denote the zero-fill left shift operator. Here x<<y simply means that the resultant x will appear with the bits shifted to the left by y places. The 0’s are inserted as new bits on the righthand side. Signed right shift: You use ‘>>’ to denote the signed right shift operator. Here x>>y simply means that the resultant x will appear with the bits shifted to the right by y places and fills 0 on voids. Let us understand this better. Consider two numbers 3 and 5. Suppose I represent them in 8-bit numbers. Now,3 can be represented in binary as 0000 0011 5 can be represented in binary as 0000 0101 Applying Bitwise OR on these numbers. 0000 0011 0000 0101 _______________ 0000 0111 So, the result will be: 1*20+ 1* 21 +1* 22 =1+2+4=7 Let us test this in Python shell now. >>> 3|5 7 Applying Bitwise AND on the numbers (3 and 5) now. 0000 0011 0000 0101 _______________ 0000 0001 So, the result will be: 1*20 =1 Let us test this in Python shell now. >>> 3&5 1 Applying Bitwise XOR on the numbers (3 and 5) now. 0000 0011 0000 0101 _______________ 0000 0110 So, the result will be: 0*20+ 1* 21 +1* 22 =0+2+4=6 Let us test this in Python shell now. >>> 3^5 6 Understanding bitwise NOT operator is not very easy. Here are two most important points: You need to consider two’s complement binary when you apply these operators in Python. A two's complement binary is the same as the classical binary representation for positive integers, but it is slightly different for negative numbers. Negative numbers are represented by performing the two's complement operation on their absolute value. Negative numbers are written with a leading one instead of a leading zero. So, if you are using only 8 bits for your twos-complement numbers, then you treat patterns from "00000000" to "01111111" as the whole numbers from 0 to 127, and reserve "1xxxxxxx" for writing negative numbers. A negative number, -x, is written using the bit pattern for (x-1) with all the bits complemented (switched from 1 to 0 or 0 to 1). So, -1 is complement (1 - 1) = complement (0) = "11111111", and -10 is complement (10 - 1) = complement (9) = complement ("00001001") = "11110110". This means that negative numbers go all the way down to -128 ("10000000"). As per the previous bullet point, you can say: -4 = Complement for (4-1)= 3 , which says ~3 is -4 -6 = Complement for (6-1)= 5, which says ~5 is -6 Therefore, in the Python shell you see the following results: >>> ~5 -6 >>> ~3 -4 In this context, it is useful to note that you can see the binary representation of a number in string format using the in-built bin() function in Python. Here are some examples for you: >>> bin(2) '0b10' >>> bin(3) '0b11' >>> bin(~3) '-0b100' >>> bin(-4) '-0b100' >>> bin(5) '0b101' >>> bin(~5) '-0b110' >>> bin(-6) '-0b110' POINTS TO NOTE The link https://wiki.python.org/moin/BitwiseOperators also says that Python doesn't use 8-bit numbers. To remove portability issues, now it considers an INFINITE number of bits. Thus, the number -5 is treated by bitwise operators as if it were written "...1111111111111111111011" Applying << on the numbers 3 and 5: 3 is in binary: 0000 0011 Now 3<<2 becomes: 0000 1100 i.e.,12 5 is in binary: 0000 0101 So, 5<<1 becomes 0000 1010 i.e., 10 Let us test these in a Python command shell now. >>> 3<<2 12 >>> 5<<1 10 Applying >> on the numbers 3 and 5: 3 is in binary: 0000 0011 Now 3>>2 becomes: 0000 0000 i.e.,0 In the same way, 5>>1 becomes 0000 0010 i.e., 2 In the same way, 5>>2 becomes 0000 0001 i.e., 1 Let us test this in Python shell now. >>> 3>>2 0 >>> 5>>2 1 >>> 5>>1 2 Precedence of Operators The operators follow the hierarchy which is shown in the following table. I’m showing the precedence of the common operators in decreasing order. Please note that parenthesis is not an operator but it has the highest precedence. I do not want you to miss this information. See Table 2-2 for your reference. Table 2-2 Operator precedence Example: >>> 3*4**2+2 11.0 Explanation: In this expression, ** has the highest precedence here. So, 4**2 computes first and we get 16. And the resultant expression becomes 3*16+2 In this expression, * has the highest precedence. So, 3*16 will compute now and you get 48. So, now the expression becomes 48+2 which is 50 Example: >>> 1+3&5 4 Explanation: As per the precedence table (See Table 2-2 ) shown earlier, arithmetic operators have higher precedence than bitwise operators. So, the given expression evaluates in the following order: 1+3&5 =4&5 =4 Operators Associativity Sometimes in an expression, you may see the operators with the same precedence. Most of the operators except exponential operator ( ** ) evaluates from left to right. In this context, you can remember that you used assignment operators for variables, such as x=5 . This evaluates from right to left. Example: >>> 5*8%3 1 Here * and % have the same precedence and they both evaluate from left. So, 5*8%3 becomes 40%3 which is 1 . [Notice that, if you evaluate 8%3 first, you’ll get the final result as 5*2=10, which is not correct]. Example: >>> 10*2//3 6 Explanation: In this expression, * and // have the same precedence. Also, both evaluate from the left. So, 10*2//3 becomes 20//3 which is 6 . Note that you can always change the order of the evaluation if you use parenthesis as follows: 10*(2//3), you get 0 . See the following in a Python Command shell: >>> 10*(2//3) 0 Example: >>> x=10 >>> y=20 >>> z=30 >>> (x<y) & (y>z) False >>> (x<y) | (y>z) True Explanation: Here I present this example to show that you can apply bitwise operators on conditional statements (that includes comparison operators). Here x<y is True , but y>z is False . And True & False results False , but True|False results True . Identity Operators You can use ‘ is’ and ‘ is not’ to test whether two objects are the same. Since I discuss class and objects in Chapter 11, I do not include the discussion here. Membership Operators You use the operators to test for the membership. In simple words, you can use ‘in’ and ‘ not in’ to test whether an element is present in a sequence. In Chapter4, you’ll see the usage. So, I do not include the discussion here. EXERCISE E2.1 Predict the output. x='12.2' print('x') print(x) E2.2 Predict the output. x=23 y=10 print(x) print(y) print(z) E2.3 How can you assign multiple variables in a single statement? Give an example. E2.4 Identify the invalid statements among the following: i)abc=1 ii)if=2 iii)$fi=3 iv)_public=4 v)abc$=5 vi)bob's=6 vii)emp_id=7 viii)emp id=8 E2.5 Write a program to output the following lines where you use a single variable to store the name(s). Welcome our favorite hero: John He is changing his name to: Sam E2.6 Predict the output of the following expression: 2+3*(36/9+1)**2-1 E2.7 Predict the output of the following expression: 3+2&4|2 E2.8 Which one you prefer -more comments inside your code, or fewer comments inside the code? E2.9 Write a python program to print the following shape: Solution to Exercises E2.1. x 12.2 E2.2. 23 10 Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter3/chap3_quiz.py", line 11, in <module> print(z) #error NameError: name 'z' is not defined E2.3 x,y,z=1,2,3 E2.4 i)abc=1 #ok ii)if=2 #error; ’if’ is a Python keyword. iii)$fi=3 #error iv)_public=4 # will work, but not recommended v)#abc$=5 #error vi)bob's=6 #error vii)emp_id=7 #ok viii)emp id=8 #error E2.5 name="John" print("Welcome our favorite hero:") print(name) name="Sam" print("He is changing his name to:") print(name) E2.6 >>> 2+3*(36/9+1)**2-1 76.0 Explanation: In this expression, () has the highest precedence. So, (36/9+1) computes first. In this sub-expression, / has higher precedence than +. So, it results in 4.0+1=5.0 and the resultant expression becomes 2+3*5.0**2-1 In this expression, ** has the highest precedence. So, 5.0*2 will compute now. It results in 25.0 and the resultant expression becomes 2+3*25.0-1 In this expression, * has the highest precedence. So, 3*25.0 will compute now. It results in 75.0 and the resultant expression becomes 2+75.0-1 Now + and – has the same precedence in the resultant expression. Both these operators are left-associative (evaluates from left to right). So, the addition will be done first and the resultant expression becomes 77.0-1, which produces the final output as 76.0 E2.7 >>> 3+2&4|2 6 Explanation: In this expression, + has the highest precedence, then & and the |. So, the expression evaluates as follows: 3+2&4|2 =5&4|2 =4|2 =6 E2.8 It depends. If a comment helps another developer to understand or review your code, it has significant value. But you need to use your intelligence. For example, you do not want to include too many unnecessary comments to describe a code that is easy to understand. E2.9 General Comments for Case Studies Before you implement the projects or case studies, I want you to note the following points: You’ll see the supporting comments in all these implementations. I also add some useful information at the beginning of a project. I use them to help you understand the important segments of code in a project. You get the complete code at the end of a chapter. I already discuss these codes (or similar codes) in the chapter or a previous chapter. So, I do not repeat the code explanations. If you do not understand a line of code, read the chapter again. This simple activity helps you to refresh your knowledge. In Python programming, you often organize the code using functions. You call them to perform the intended job and make your code more Pythonic. In chapter7, you’ll learn about functions in detail. Once you learn them, you can beautify the initial implementations I show you in this book. You need to guard your application against unwanted user inputs/scenarios. Chapter 8 teaches you how to do that. I show you the simple solutions at the beginning. It is because your initial aim is to meet the least essential requirements. You can ignore the remaining corner cases when you implement them for the first time. Later you can improve these projects. For example, you can ignore invalid user inputs for the initial case studies. After you complete reading Chapter 7 and Chapter 8, you can consider those cases and improve your solutions. Case Study I CS 1.1 Problem Statement Welcome to your first project. As a creator, you always like to imagine the final product in advance. Here I want you to develop a company catalog. Assume that this company sells three different dry fruits-Apricot, Dates, and Almond. The seller can sell individual items or a combination of these items. A gift pack is a special combination that contains all three items. Here are some special considerations: If a customer purchases individual items, he does not receive any discount. If a customer purchases a combo pack with two unique items, he gets a 10% discount. If the customer purchases a gift pack, he gets a 25% discount. For illustration purposes, I choose an Indian retailer (Spencer). I also show the price of an item in Indian Rupees. The final output should look like the following (See Figure CS1.1-1): Figure CS1.1-1:The final output of the project. Are you ready? Chapter 3: Common Data Types This chapter shows you the usage of some common data types and the builtin functions in Python. In simple words, a function is a block of code that helps you to perform an intended job. Using the words “built-in functions”, I mean that these are already written and available for you. So, you can directly use them in your program. You’ll learn more details about functions later in the Chapter7 of this book. There are many data types in Python. As a beginner, to proceed further, you need to be familiar with strings, numbers, and booleans. These are the most common data types, and you see them almost in every program. We’ll cover them quickly in the upcoming sections. Instead of creating separate files for each of these small code segments, I place them into a single file. I also keep the comments for your reference. If you want, you can write each segment in separate files, or you can simply execute them in a Python shell. All are fine. Code Demonstrations with Strings In Chapter2, I gave you a brief introduction to strings. Here you’ll know more about them. Any string can include letters ( A-Z, a-z ) and digits ( 0-9 ). Other printable characters such as &,$, *, and# can be included in a sting too. You can also include special characters (often termed as control codes) followed by a backslash (\). For example, \n represents an escape character. When you use it in a string, the character ‘n’ is escaped, and the text cursor moves down to the next line. Here is an example for you: >>> print("Hello\nWorld!") Hello World! Similarly, if you want to print a tab between letters, you can use \t . Here is an example: >>> print("Hello\tWorld!") Hello World! The \n and \t are very common. Apart from these, you may see the use of \b for backspace, \a for sounding a beep sound, etc. But the behavior of \ b and \a can vary. To illustrate, each of them works fine in a Python command shell, but they do not work in IDLE as per the previous description. To show this, let us use a Python command shell first: >>> print("Abc\bd") Abd Now execute the same line of code in IDLE. In this case, you see the following output: >>> print("Abc\bd") Abcd You can see the difference! Sometimes, you may not want backslash ( \ ) to use for escape characters. For example, you may want to print the string with the backslash. In this case, you can use raw strings by adding an r (or R) before the double quotation as follows: >>> print(r"Hello\World") Hello\World Before you read further, I want you to note another particular type of Python statement. I use this approach in various examples and projects in this book to decorate top borders or bottom borders. Here is a sample statement: print("*"*10) How does it work? This statement prints the star 10 times. In the same way, you can use: print("-"*15) to print the character ‘ –‘ 15 times. What is the benefit? You can type less and make your code size short. I show you some sample use cases of this for your immediate reference. >>> print("*"*10) ********** >>> print("-"*15) -------------->>> print("#"*5) ##### Now I’ll show you some other common use cases of strings. Go through the following code fragments: Goal: I want to print HelloWorld! inside single quotations. Code: print("'HelloWorld!'"); Expected Output: 'HelloWorld!' Goal: I want to print HelloWorld! inside double-quotations. Code: #Trying to print HelloWorld! inside double quotations print(""HelloWorld""); #Error Expected Output: print(""HelloWorld""); #Error ^ SyntaxError: invalid syntax Explanation: To handle this case, you can use escape characters like the following: #Using the escape characters print(" \"Hello World! \" ") Here you simply tell Python to insert the character (which is a double quotation in this case) after the backslash. Now you can get the following output: "Hello World! " Alternatively, you can use double quotations inside single quotations as follows: >>> print(' "Hello World!" ') "Hello World!" Goal: I want to print HelloWorld! using a string variable. Code: my_text="Hello World!" print(my_text) Expected Output: Hello World! Explanation: You are familiar with this code. Here you print the text message using a string variable named my_text . Goal: I want to concatenate multiple strings. Code: # Concatenation example text1 = "Hello, Reader!" text2 = " How are you?" text3 = " Hope everything is fine." print(text1+text2+text3) Expected Output: Hello, Reader! How are you? Hope everything is fine. Explanation: Here you see the plus (+) operator is in action. Using this, you can concatenate strings. This activity is common in computer programming. Now I show you the usage of some built-in functions. You will see a detailed discussion on functions in Chapter7 of the book. I told you before that a function is a block of code to perform an intended job. You can write your function(s) or, you can use the functions that are already written in Python. I also told you that by using the words “built-in functions”, I mean that you can directly use them in your code. Now the obvious question is: how will you know about the available functions? The use of an IDE can help you in this case. You know that I am using PyCharm IDE for this book. It gives me some special supports when I invoke (or, call) the functions. For example, suppose, I have the following code: text1="Hello, Reader!" Now when I write text1 and then put a dot(.), I can see available functions for me. Here is a screenshot for you: Figure 3-1:Showing available functions when you type a string variable and put a dot(.) Let us test some of these functions. Goal: I want to print HelloWorld! in uppercases and lowercases. Code: text1 = " Hello, Reader!" print("The original string is:" + text1) # Printing text1 in uppercase print(text1.upper()) # Printing text1 in lowercase print(text1.lower()) Expected Output: The original string is: Hello, Reader! HELLO, READER! hello, reader! Explanation: You can see the function upper() is converting the original string into uppercase characters, and lower() is doing the opposite. Goal: I want to use multiple functions together. Code: # Using multiple functions together text1 = " Hello, Reader!" print("The original string is:"+ text1) print(text1.upper().islower()) #False print(text1.upper().isupper()) #True Expected Output: The original string is: Hello, Reader! False True Explanation: This fragment of code shows that you can use multiple functions together. For example, text1.upper().islower() is actually doing two things: At first, it performs text1.upper() which converts the string into uppercase characters, and then it invokes the islower() function on the resultant string. The islower() function verifies whether the string is in lowercase or not. Since the original string is converted in uppercase characters already, it returns False . The next line of code is testing the reverse scenario. The isupper() function is used to test whether the resultant string is in uppercase or not. Before you invoke this function, you transformed the string to uppercase characters. This is why you get True in the output. Goal: I want to calculate the length of a string. Code: text1 = "Python" text2 = "Hello, Jon!" print("The text1 is: " + text1) print("Length of text1 is as follows:") #Length of text1 print(len(text1)) print("The text2 is: " + text2) print("Length of text2 is as follows:") #Length of text2 print(len(text2)) Expected Output: The text1 is: Python Length of text1 is as follows: 6 The text2 is: Hello, Jon! Length of text2 is as follows: 11 Explanation: The len() function is used to calculate the length of a string. Here I have calculated the length of two different strings using this function. POINTS TO REMEMBER Note that I’ve used len(text1) and len(text2). Programmatically, when you write similar codes, you say that you pass text1 and text2 as method arguments (often called parameters). Expert programmers are particular about the terminologies. Ideally, in this case, you should say that len() is a function that can accept a string parameter. But when you use len(text1) , you should say that I’m passing a string argument text1 inside the len() function. I’ve included a discussion on this in Chapter7. Goal: I want to examine the index positions of the characters inside a string. Code: text1 = "Python" print("The text1 is: " + text1) # Printing individual characters inside the string print(text1[0]) print(text1[1]) print(text1[2]) print(text1[3]) print(text1[4]) print(text1[5]) Expected Output: The text1 is: Python P y t h o n Explanation: Here I have shown you all the index positions from 0 to 4. Notice that the array indexing starts from the 0th position. So, a,b,c,d, and e are stored at index positions 0,1,2,3 , and 4 respectively. This is why, text1[0] prints the first character a, text1[1] prints the next character, and so on. You see the same in many other high-level languages, such as Java, C++. Goal: I want to get the first occurrence of a character (or, a word) inside a string. Code: text = "abcABc" print("The text is: " + text) # Printing individual characters inside the string print("The first occurrence of 'A' is at index:") print(text.index("A")) print("The first occurrence of 'c' is at index:") print(text.index("c")) print("The first occurrence of 'bcA' is at index:") print(text.index("bcA")) Expected Output: The text is: abcABc The first occurrence of 'A' is at index: 3 The first occurrence of 'c' is at index: 2 The first occurrence of 'bcA' is at index: 1 Explanation: As shown here, you can use the index() function to retrieve the first occurrence of a particular character inside a string. Notice that inside the string, we had two c’s; one is at index 2 and another is at 5 . But the function returns the index position 2 . Also, I searched for the index position of the combined characters bcA . So, you can use the same function to find a particular word inside a string too. Goal: I want to examine whether an intended character is absent inside a string. Code: text="Hello, John!" print(text.index("y"))#error now Expected Output: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter3/string_usage_file_2.py", line 16, in <module> print(text.index("y")) #error now ValueError: substring not found Explanation: The error message is self-explanatory. You see this error because the character y is not present inside the string Hello, John! Goal: I want to examine a function that can has multiple parameters. This is why I’m showing you an example using the replace() function. Code: # I want to examine a function that accepts multiple # parameters.Using the replace() function for this example. text = " Hello, John!" print("Initial text is :" + text) print("Replacing the name 'John' with 'Bob' now.") text = text.replace("John", "Bob") print("The changed text is :" + text) Expected Output: Initial text is : Hello, John! Replacing the name 'John' with 'Bob' now. The changed text is : Hello, Bob! Explanation: Notice that the replace() function takes two arguments. I use the first one for the string which I replace, and the second one I use for the string which reflects the changed value. So, to change the name John with Bob , I have used text.replace(“John”, “Bob”) , and I hold this changed value into the same string variable text . Code Demonstrations with Numbers In Chapter2, I gave you a brief introduction to Numbers. In this section, you are going to explore more about them. Before I proceed further, it’s worth remembering some important points which are as follows: Like strings, to print a number without a variable, you can just type the number inside the print() function. See the following code segment with inline comments. print(1)#Prints 1 print(5.7)# Prints 5.7 print(-6.789)# Prints -6.789 You can perform both the basic and complex arithmetic operations inside the print statement. In chapter2, you learned about the precedence and associativity of operators. If you understood the concept, you face no problem to predict the output in advance. Consider the following code segment with comments for your quick reference: #Performing some basic and complex arithmetic operations print(1+2) #Prints 3 print(10 - 3) #Prints 7 print( 25* 3) #Prints 75 print(12.88/4) #Prints 3.22 print(1+2*3)#Prints 7 print((1+2)*3) #Prints 9 You have also used variables for numbers. Here is a sample code segment to examine the usage of number variables: # Using variables my_int=125 print(my_int)# Prints 125 my_float=25.763 print(my_float)# Prints 25.763 Remember that for string variables, the plus (+) operator concatenates the strings, but for numbers, it adds them. Here is a sample for you: #Difference between number and strings my_string1="10" my_string2 = "22" my_int1=10 my_int2=22 print(my_string1+my_string2) #Prints 1022 print(my_int1+my_int2) #Prints 32 One question may appear in your mind: whether we can use both numbers and strings together inside a print statement? The answer is yes, and you need to do this very often. A simple solution is to write something like: print("The value inside my_int1 is:", my_int1) Which can produce the following output: The value inside my_int1 is: 10 You can follow various approaches to print the output in the console. For example, you can use comma’s inside the print() function. You can also use string concatenation (using + operator). You can also use string interpolation (using the format () functions) too. From Python 3.6, format literals (it uses prefix f) are also available. You’ve just started learning Python programming. So, let us stick to the simple forms at this stage. I show you f-strings usage at the end of this chapter. But if you want to use the + operator, you need to do a trick, in which you convert them into the same datatype. Otherwise, you’ll see errors. For example, if you write: print("The value inside my_int1 is :" +my_int1) #error You’ll get the errors like: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter3/numbers_usage_file_1.py", line 38, in <module> print("The value inside my_int1 is :" +my_int1) TypeError: can only concatenate str (not "int") to str The error is self-explanatory. You see this error because you can do concatenation for strings, but not for a string and a number. Here my_int contains a whole number (10). So, programmatically it is an int. So, you see the word ‘int’ in the error message. (When a number has a fractional part, say 10.2, it is a float number. I discussed the difference between int and float in Chapter2). Again, if you write and execute the following line of code: print(my_int1 + " is my first number") #error You will again receive an error: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter3/numbers_usage_file_1.py", line 38, in <module> print(my_int1 + " is my first number") #error TypeError: unsupported operand type(s) for +: 'int' and 'str' This time in the print function, you place the number variable before the string variable. So, you see this error. What is the solution? You have guessed it right! Make both of them the same data type. For example, you can convert the number into a string using the str() function as follows: print("The value inside my_int1 is:" +str(my_int1)) #Ok If you execute this code, you can receive the following output now: The value inside my_int1 is:10 Now I show you a function to convert a string to an integer. See the following code segment. my_string1="12" #Converting a string to an int print(int(my_string1,10)) #prints 12 Here 10 is used to state the base of the number. But all strings are not convertible to an integer. For example, if you try to execute the following code: # All strings are NOT convertible to an int my_string2 = "abc" print(int(my_string2, 10)) # Error You’ll receive the error like the following (the line number can vary in your file): Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter3/numbers_usage_file_1.py", line 48, in <module> print(int(my_string2, 10)) # Error ValueError: invalid literal for int() with base 10: 'abc' You can use the isdigit() function to test whether a string is convertible to a true digit or not. For example, if you execute the following code segment: my_string1="25" my_string2 = "abc" print(my_string1.isdigit()) # True print(my_string2.isdigit()) # False you’ll receive the following output: True False If you use PyCharm IDE, when you move your cursor on the function, you can see the function definition easily. For your easy reference, I take a screenshot from this IDE when I move my cursor on the isdigit() function (See Figure 3-2): Figure 3-2: Description of isDigit() function from a PyCharm IDE Or, you can press Ctrl+Shift+I to get the following screen (See Figure 3-3) Figure 3-3: Retrieving the definition of isDigit() function Now see Show Source(Ctrl +Enter) to get the details (See Figure 3-4): Figure 3-4: Retrieving the description of isDigit() function from a PyCharm IDE These simple activities can make your programming life easy. This is another valid use case, which shows the effectiveness of an IDE over a normal Python shell. Quick talk on f-strings Python 3.6 introduced the concepts of the f-strings. You can put the letter “f” in front of a string and then inject a variable into it. I like this approach because it is easy to read and understand. To inject a variable inside a string, you need to wrap it inside the curly brackets. Let us look at the following example: user_name="John" print(f"Hello, {user_name}!") This code segment can produce the following output: Hello, John! Now you are ready to examine some built-in functions for numbers. Let us proceed. Goal: I want to round a number. Code: # Rounding a number print(round(2.7)) # Prints 2 print(round(5.32)) # Prints 5 Expected Output: 3 5 Goal: I want to find the maximum and minimum from a set of numbers. Code: #Finding the maximum print(max(1,2,3,4,5)) # Prints 5 #Finding the minimum print(min(1,2,3,4,5)) # Prints 1 Expected Output: 5 1 Explanation: You can see the max() function is used to find the greatest among the numbers and the min() function is used to find the smallest among the numbers. Now I import some math functions and use those functions. So, at the beginning of the file, I write the following: # importing math functions from math import *; It helps me to get the functions sqrt(), ceil(),floor() etc. If you do not import the math functions, you cannot use (or access) these functions. So, if you see the source (For example, as said before, put your cursor on math and press Ctrl+Shift+I ), you can see a screen like the following (see Figure 3-5): Figure 3-5: A partial snapshot of available math functions And if you go through the file, you can see the functions named ceil(),floor(),sqrt(),gcd(),factorial() etc. Let us use some of them in the following section: POINT TO REMEMBER As said before, do not forget to import the math functions before you use the following functions. In Chapter 7, you’ll learn about modules and see import statements. Using an import statement, you make the previously written codes available in a current file. For example, in many project implementations, I use: from random import randint It helps me to get the in-built randint() function from the random module. Once I import this statement, I can generate a random number using this function. For example, randint(1,15) can generate a random number between 1 and 15, including both endpoints. Goal: I want to get the square root of a number. Code: # Printing the square root of 25 print(f"Square root of 25 is:{sqrt(25)}") # 5.0 # Printing the square root of 6.25 print(f"Square root of 6.25 is:{sqrt(6.25)}") # 2.5 Expected Output: Square root of 25 is:5.0 Square root of 6.25 is:2.5 Goal: I want to find the ceiling value and the floor value of a number. Code: # Printing the ceiling value # This is the smallest integer less than the number # you defined. print(f"The ceiling value of 39.3 is:{ceil(39.3)}") # 40 # Printing the floor value # This is the biggest integer NOT greater than the number # you defined. print(f"The floor value of 39.3 is: {floor(39.3)}") # 39 Expected Output: The ceiling value of 39.3 is:40 The floor value of 39.3 is: 39 Explanation Refer to the comments for your understanding. Goal: I want to find the gcd (greatest common divisor) of two numbers. Code: # Finding the gcd of 4 and 14 print(f"The gcd of 4 and 14 is:{gcd(4,14)}") # prints 2 # Finding the gcd of 14 and 63 print(f"The gcd of 63 and 14 is:{gcd(63,14)}") # prints 7 Expected Output: The gcd of 4 and 14 is:2 The gcd of 63 and 14 is:7 Goal: I want to find the factorial of a number. Code: # Finding the factorial of 5 print(f"The factorial of 5 is: {factorial(5)}") # Finding the factorial of 7 print(f"The factorial of 7 is: {factorial(7)}") Expected Output: The factorial of 5 is: 120 The factorial of 7 is: 5040 Explanation: The factorial of 5 is: 5*4*3*2*1=120 The factorial of 7 is: 7*6*5*4*3*2*1=5040 Accepting User Inputs Now I show you how to accept user input in a program. You use the input() function in this context. Using this function, you can pass an optional prompt. The prompt can help a user know about the information he needs to supply. When you run the following demonstration, you’ll see that the input() function pauses the program and waits for the user’s input. Once you supply the input, you can proceed further. Demonstration 1 To examine this, let us write a simple program where the user can supply his name and age. I store these inputs inside some variables. Then I print the information with some additional messages. Let us type the following lines in your Python file and save the content. Then run the program. # Supply a name user_name = input("Enter your name:") # Enter the age user_age = input("Enter your age:") # Using commas print("Welcome,",user_name, "! you are now ", user_age) # Using f-strings(Python 3.6 onwards) print(f"Welcome,{user_name}! you are now {user_age}") # Printing using string concatenation print("Welcome," + user_name + "! you are now " + str(user_age)) Output You should see the following output: Enter your name:John Enter your age:25 Welcome, John ! you are now 25 Welcome,John! you are now 25 Welcome,John! you are now 25 You have seen how to supply user inputs to your program. This program also shows the different ways to print the output. EXERCISE E3.1 Write a python program to accept two numbers from a user and print the average of them. You can avoid the input validation until you read chapter8. E3.2 Write a program to output the following using exactly one print statement. Hello, Reader! E3.3 Print the following line. You can test it using single quotations or double-quotations. The height of Andrew is 6'9" E3.4 Given the following assignment: x,y=10,2.5 Predict the output of the following Python statements: print(x+y) print(x/y) print("Difference of x and y is:", x-y) print("x*y=:"+ x*y) print(x+ "is stored in x.") E3.5 Predict the output of the following Python code segment: x = 12 #print("x= "+12) E3.6 Write a python program to calculate the area of a circle. Your program should accept the radius from a user. You can assume that the user provides valid inputs only. Solution to Exercises E3.1 #Solution to Assignment 3.1 #First user input first_input=input("Enter first number:") #Converting it to float first_number=float(first_input) #Second user input second_input=input("Enter second number:") #Converting it to float second_number=float(second_input) #Calculating the average average=(first_number+second_number)/2 print("Average is :",average) Output Enter first number:25.2 Enter second number:75 Average is : 50.1 E3.2 #Solution to Assignment 3.2 print("Hello,\nReader!") E3.3 print('The height of Andrew is 6\'9" ') Alternative answer: print("The height of Andrew is 6'9\"") Output: The height of Andrew is 6'9" E3.4 The output for each of the following statements is shown with inline comments: x,y=10,2.5 print(x+y) #12.5 print(x/y) #4.0 print("Difference of x and y is:", x-y) #7.5 #The following line of code will cause error: can only #concatenate str (not "float") to str print("x*y=:"+ x*y) # The following line of code will cause error: unsupported #operand #type(s) for +: 'int' and 'str' print(x+ "is stored in x.") E3.5 It will print nothing because the print statement is commented. E3.6 #Get the radius from the user and converting it into a float radius = float(input("Enter the radius of the circle:")) #Area of a circle=(22/7)*r*r where r is the radius area = (22/7)*radius*radius print(f"The area of the circle is:{area} square unit.") Output: Enter the radius of the circle:7.7 The area of the circle is:186.34 square unit. CS 1.1 Implementation Here I use f-strings to print the seller’s name, address, etc. So, you see the following codes: print(f"{seller_name}") print(f"{seller_address}") In this implementation, you do not need these f-strings. I could use hard-coded strings like: print("Spencer Retail") print("136, Garia Station Road,\n Kolkata:700084") But the problem is that when you need to update the seller information, you need to reflect the change in every place. So, I suggest that you use variables in similar places for better maintenance. In that case, you can update the variable in one place and it will reflect the updated result in all locations. Now go through the complete implementation. # Seller information seller_name = " Spencer Retail" seller_address = " 136, Garia Station Road,\n Kolkata:700084" seller_contact = "123-456-789" #Decorating top segment print("-" * 50) print(f"{seller_name}") print(f"{seller_address}") print("-" * 50) apricot_price = 300 dates_price = 400 almonds_price = 500 apri_dates_combo = (apricot_price + dates_price) * .9 # 10% discount dates_almon_combo = (dates_price + almonds_price) * .9 # 10% discount almon_apri_combo = (almonds_price + apricot_price) * .9 # 10% discount gift_pack = (apricot_price + dates_price + almonds_price) * .75 # 25% discount print("Product(s) \tPrice (per pack)") print(f"Apricot\t\t{apricot_price}") print(f"Dates\t\t{dates_price}") print(f"Almond\t\t{almonds_price}") print(f"Combo-1\t\t{apri_dates_combo}") print(f"Combo-2\t\t{dates_almon_combo}") print(f"Combo-3\t\t{almon_apri_combo}") print(f"GiftBox\t\t{gift_pack}") # Decorating the bottom segment. # It contains the contact information. print("*" * 50) print(f"For free delivery, contact {seller_contact} ") print("*" * 50) Case Study II CS 2.1 Problem Statement Will you like to attend a test? The test is simple. You need to predict the value of an expression in advance. You can attend this test as many times as you want, but there is a twist. The computer can show you a unique expression each time you play the game. Are you ready? For a better understanding, I’m supplying some sample output for you: Case-1: Wrong prediction Predict the value of the following expression: 12*8%3 Enter your answer:5 Your answer is wrong. The correct answer is:0 Case-2: Correct prediction Predict the value of the following expression: 12+(2*3)/4 Enter your answer:13.5 Correct answer. Author’s Comment: In this project, you can consider only one expression and change the data inside it. In that case, you can make a working solution at the end of Chapter 2. But I want you to make an improved solution. A better application can test you with different expressions using varying data. So, I show you this implementation at the end of Chapter 4. Chapter 4: Decision Making Decision making is an integral part of programming. You often need to examine certain conditions in a program. Based on those conditions, you control the flow of the program execution. This chapter teaches how to respond based on the state of your program. We often perform conditional tests using various if -statements. For example, you may see a simple if statement, an if-else chain, or an if-elifelse chain in a program. The choice depends on how many conditions you want to test at a specific point. In the chapter2, you have seen the use of comparison operators. This chapter uses those operators to verify these conditions. Using an if Statement Let us begin with an easy program. Here I assume that I have a number variable. If its value is greater than 10, the program can tell that the current value of the variable is bigger than 10. Demonstration 1 Let us create a new file and type the following into it: a = 11 if a > 10: print("The current value inside a is greater than 10.") Output When you run this program, you’ll see the following output: The current value inside a is greater than 10. Using the if-else Statements There is a potential drawback in the previous program: if a is less than or equal to 10, it will NOT print any output. (To test this, you can change the value of a to a value less than 10 and execute the program again.) Demonstration 2 Let us make the program better. Now I add a few more lines to the previous demonstration. This time you see the use of an else statement. Go through the following code. else: print("The current value of a is less than or equal to 10.") Now you can get an output if the value of a is less than 10. To test this, let us make a=5 and run the following program: a=5 if a > 10: print("The current value inside a is greater than 10.") else: print("The current value of a is less than or equal to 10.") Output This time you’re expected to see the following output: The current value of a is less than or equal to 10. Demonstration 3 Notice the code in the previous demonstration. You understand that inside the if-statement, I’m testing a condition. If the condition is true, then the indented lines following the if-statement will execute. Otherwise, the indented lines following the else statement will execute. So, you understand that the if-else chain helps you to check whether a certain condition is True or False. Let us see another example. This time I check whether a value inside a variable is equal to the value of my interest. To make it clear, go through the following example: a=5 if a == 5: print("a is equal to 5.") else: print("a is NOT equal to 5.") Output We use a single equal to (=) sign is to assign a value to a variable. For example, you can read the code: a=5 as “ Set the value 5 to the variable a ”. You have seen this kind of usage several times. But we use double equal to ( == ) for the equality operator. It returns True if values of both sides of the operator match; otherwise, it returns False . So, when you run the previous program, you’ll get the following output: a is equal to 5. POINT TO REMEMBER The online link: https://docs.python.org/release/3.0.1/whatsnew/3.0.html#changedsyntax says that True, False, and None are reserved words. In Python3.x programming, when you convert int to bool, the boolean value is True for the integers except 0. For better clarity, I show you a few more code fragments. I executed these in a Python3.8.3 command shell: >>> int(False) 0 >>> int(True) 1 >>> bool(-3) True >>> bool(5) True >>> bool(0) False Demonstration 4 In the chapter2, you have seen that the != operator does exactly the opposite to the == . (The exclamation sign is for “not”; so, you pronounce “!=” as “Not equal to”. You may also see similar syntaxes in many other programming languages like Java, C#, etc.) Now I make some minor changes. Here an if statement checks a “Not equal to” condition instead of an “Equal to” condition. Consider the following code segment: a = 25 if a != 7: print("a is NOT equal to 7.") else: print("a is equal to 7.") Output If you run this program now, you’ll get the following output: a is NOT equal to 7. Using elif with an if-else Statement You have seen the uses of if , if-else , ==, and != . Let us fine-tune the previous programs now. This time you see the use of the elif statement. It is very common when your program needs to check over two conditions. In such a case, using if-else statements is not enough. To understand this, consider another case. Let us assume instead of saying ‘ Greater than equal to ’ (or, less than equal to ), you segregate the case ‘ Equal to’ . For example, let us suppose, in your application, the user supplies a number. Now your program should print whether the number is greater than 10, or less than 10, or equal to 10. The if-elif-else chain can handle such a scenario. Demonstration 5 Let us type the following lines of code and execute the program. For simplicity, I’m skipping the validation of user input. I assume that he follows the instruction properly, and he does not supply any invalid data. user_input = input("Enter a valid number only:") # Skipping the validation of the user's input a = float(user_input) if a > 10: print("The number is greater than 10.") elif a == 10: print("The number is equal to 10.") else: print("The number is less than 10.") Output Here are some outputs for different user-supplied numbers. Case1: Enter a valid number only:15 The number is greater than 10. Case2: Enter a valid number only:6 The number is less than 10. Case3: Enter a valid number only:10 The number is equal to 10. Case4: Enter a valid number only:7.9 The number is less than 10. POINTS TO REMEMBER Python executes only one block from an if-else or if-elif-else chain. The control flows in the sequential order until one condition becomes True . You may see many indented lines of code under any of these blocks ( if, elif, or else ). When a condition becomes True , all the indented lines inside the block will execute. You can also note: the else block is not mandatory. In demonstration1, you have seen that you can write a program with an if block only. In that program, else block was not present. Like that case, else block is also not mandatory at the end of an if-elif chain. One question may come into your mind now: How to handle a scenario that considers more than 3 cases? This is also a common use case. For example, let’s assume that you are about to launch a product. Assume that you decide the following prices for different age groups: Kids up to age 10, can use the product for free. If the user is above 10 years but less than 20 years, he needs to pay only 1$. If the user is between the age of 20 years to less than 30 years, he needs to pay only 2$. If the user is between the age of 30 years to less than 40 years, he needs to pay only 3$. Users from 40 and above, need to pay 4$. You can see that you need to check more than 3 conditions in this scenario. Let us see how can we handle this using an if-elif-else chain. Before you go forward, I give you a clue. In an if-elif-else chain, Python does not restrict you to use only one elif statement. Demonstration 6 Let us type the following lines of code and execute the program. In the analysis section, you’ll get an idea about how to improve this program. Like the previous cases, for the sake of simplicity, I’m skipping the validation of user inputs. user_input = input("Enter your age:") # Skipping the validation of user's input age = float(user_input) if age < 10: print("Hi Dear. You can use the product for free.") elif age >= 10 and age <20: print("Please donate 1$ for the product.") elif age >= 20 and age < 30: print("Please donate 2$ for the product.") elif age >= 30 and age < 40: print("Please donate 3$ for the product.") else: print("Please donate 4$ for the product.") Output Here is the output for different user-supplied numbers. Case1: Enter your age:9.2 Hi Dear. You can use the product for free. Case2: Enter your age:12.5 Please donate 1$ for the product. Case3: Enter your age:25 Please donate 2$ for the product. Case4: Enter your age:37 Please donate 3$ for the product. Case5: Enter your age :57.3 Please donate 4$ for the product. Analysis This program gives you an idea about how to test many conditions using the if-elif-else chain. Apart from this, there are several points to note in this program. The following bullet points list the important points: You can improve the following code. For example, you can replace the code: elif age>= 10 and age <20: # other code using the following code: elif 10 <= age < 20: # other code Notice that I’m simplifying the expression. You can do the same in similar places inside the program. For better readability, you can use brackets. Here is a sample: elif (age >= 20) and (age < 30): print("Please donate 2$ for the product.") I’ve not used brackets in Demonstration 6 because it was NOT mandatory for me. Note that I have used only one statement inside an if block, elif block, or an else block. But it is very common when you see many print statements in any of these blocks. For example, the following block of code is perfectly fine: if age < 10: print("Hi Dear. You can use the product for free.") print("Thank you for choosing the product.") //remaining code Tautology and Contradictions You should be careful about your logic. Sometimes the result of the compound expressions is always False . For example, if you write: if a < 5 and a > 7: # This condition cannot be satisfied. print("The if condition is satisfied.") Think now. Is it possible that a variable is less than 5 but greater than 7 at the same time? The answer is no. We term these as contradictions . We term the opposite scenario as a tautology, where the resultant value of the compound expressions is always True . Alternative Designs Sometimes you may see alternative designs in a similar context. I do not recommend these practices, but I want you to note that Python does not complain if you follow them. Let us look into them. Demonstration 7 In demonstration5, you saw the use of if-elif-else chain. Here you see a program that uses the if-else chain. Write the following program and execute it. user_input = input("Enter a valid number only:") # For simplicity, skipping the validation of user's input a = float(user_input) if a > 10: print("The number is greater than 10.") else: if a == 10: print("The number is equal to 10.") else: print("The number is less than 10.") Output You can test this program against the same inputs that I used in demonstration5. Here, you’ll receive the same output. I do not repeat them here. Analysis Have you noticed that in this approach, if you need to check more conditions, the texts are drifted to the right? For example, see the following program. It is another valid program with more conditions. But I continue to use the if-else chain instead of the if-elif-else chain. user_input = input("Enter a valid numbers only:") # For simplicity, skipping the validation of user's input a = float(user_input) if a < 0: print("The number is less than 0.") else: if a < 1: print("The number is greater than 0 but less than 1.") else: if a == 1: print("The number is equal to 1.") else: if a == 2: print("The number is equal to 2.") else: if a == 3: print("The number is equal to 3.") else: print("The number is greater than 3.") You can see that as the number of conditions grows, the texts are drifting to the right side. This gives you the idea about: why do I prefer an ifelif-else chain in this case. For me, it is a better option and more manageable. The previous code segment shows you can write nested if (or else) statements too. Demonstration 8 Let us explore another way of using simple if statements to check a condition. Again, I do not recommend this. I include this for one reason. It should not surprise you when you see similar constructs in other people's code. In this program, I divide a number by another number. You know that in this case, the divisor should not be zero. So, you can write a simple program like the following: a = input("Enter a valid integer for the dividend:") b = input("Enter a valid integer for the divisor:") # Skipping the input validation... dividend = int(a) divisor = int(b) if divisor != 0: result = dividend / divisor print(f"Division successful.The result is:{result}") else: print("You cannot proceed when the divisor is 0.") This program works for you and it is easily readable. But now I write an alternative version of the previous code. We refer to this as inline-if . You can summarize the concept as: Do task-1 if the condition is True else do task-2 Here is an example for you: # Assign b before the following statement a = 5 if b != 0 else 1 print(a) The previous code segment tells that we assign the value of a to 5 if b is not equal to 0. Otherwise, we assign the value 1 to a . So, using inline-if , you can write an alternative version of the program. Notice the changes in bold: a = input("Enter a valid integer for the dividend:") b = input("Enter a valid integer for the divisor:") # Skipping the input validation... dividend = int(a) divisor = int(b) # Alternative version result= 0 # some initial value result = dividend / divisor if divisor != 0 else print("You cannot set the divisor to 0.") print(f"The result is:{result}") Output Here is the output for some valid inputs. Case1: Enter a valid integer for the dividend:24 Enter a valid integer for the divisor:3 The final result is: 8.0 Case2: Enter a valid integer for the dividend:24 Enter a valid integer for the divisor:0 You cannot set the divisor to 0. The result is: None EXERCISE E4.1 Is there any error in the following code? #Exercise-4.1 a=5 if a > 10: print("The number is greater than 10.") elif a == 10: print("The number is equal to 10.") E4.2 Predict the output. #Exercise-4.2 a=5 if a < 7 and a > 9: print("The condition is satisfied.") else: print("The condition will never be satisfied.") E4.3 Predict the output. #Exercise-4.3 number = 0 #number = -34 if number: print(f"The condition is satisfied.number={number}") else: print(f"The condition is not satisfied.number={number}") E4.4 Give an example of a tautology. E4.5 Give an example of a contradiction. Solution to Exercises E4.1 No, there is no problem because it is not mandatory to place an else block at the end of an if-elif chain. But in this case, you will not receive any output. It is because neither the condition of the if block nor the condition of the elif block is satisfied. E4.2 The condition will never be satisfied. Explanation: It is an example of a contradiction. In other words, the result of the compound expressions is always False in this case. In this program, if you replace and with or , then the if condition can be satisfied. E4.3 The condition is not satisfied.number= 0 Explanation: In Python3.x programming, when you convert int to bool, except 0, Python evaluates all to True . I have already shown you an example in the chapter. You can refer to that discussion if needed. E4.4 Let us say my_number is a number variable. So, the following expression: my_number>0 or my_number<=0 is always true. So, it is an example of tautology. E4.5 See Exercise 4.2. CS 2.1 Implementation I start with three unique expressions in this project. When you run this application, you get a unique expression in each run. For example, in the sample output you can see that in one run, you get an expression 12*8%3. But in a different run, you get a new expression: 12+(2*3)/4 . To give it more dynamic behavior, I change the data in a particular type of expression too. For example, in one run, you may see the expression: 12*8%3 , but in a different run, you may see 11*8%3. It is because I replace some data in these initial expressions with random data in each run. To do this, I import the randint() function. The randint(a,b) can return a random integer in the range [a, b] including both endpoints. I talked about this function in Chapter 3. The following code segment with supporting comments can explain it: x = randint(10, 12) question1 = "2*3-4" # Replacing 2 in question1 with x question1 = question1.replace("2", str(x)) question2 = "1+(2*3)/4" # Replacing 1 in question2 with x question2 = question2.replace("1", str(x)) question3 = "5*8%3" # Replacing 5 in question3 with x question3 = question3.replace("5", str(x)) I use another important function eval() in this implementation. This function has 3 parameters, the first one is mandatory and the last two are optional. I do not need these optional parameters now. We can pass a string to eval() that can evaluate it as a Python expression. To show how it works, I present you following code segment: >>> eval("1+2+3") 6 >>> eval("2*5+2") 12 Now you are ready to go through the complete implementation. Here is the complete code: from random import randint x = randint(10, 12) #First question with initial data question1 = "2*3-4" # Replacing 2 in question1 with x question1 = question1.replace("2", str(x)) #Second question with initial data question2 = "1+(2*3)/4" # Replacing 1 in question2 with x question2 = question2.replace("1", str(x)) #Third question with initial data question3 = "5*8%3" # Replacing 5 in question3 with x question3 = question3.replace("5", str(x)) # The randint(1,3) function returns a number # between 1 and 3 (both included) pick_question = randint(1,3) if pick_question == 1: quiz = question1 elif pick_question == 2: quiz = question2 else: quiz = question3 print(f"Predict the value of the following expression:{quiz}") # User's input user_input = input("Enter your answer:") # Converting it to float predicted_value = float(user_input) actual_value = eval(quiz) if predicted_value == actual_value: print("Correct answer.") else: print("Your answer is wrong.") print(f"The correct answer is:{actual_value}") Case Study III CS 3.1 Problem Statement Will you like to play a game? It is simple but interesting. The computer will pick a number between 1 to 15 for you. You need to guess the number. But here is the challenge. To win the game, you need to make a correct guess within three attempts. Note another point: Each time you play the game, the computer can pick a different number for you. Are you ready? For a better understanding, I show you some sample output: Case-1: You have lost the game. The computer has picked a random number for you. Clue: It is between 1 and 15 (both inclusive). Can you guess it within 3 attempts? Make a try. Enter your answer:5 It is high. Try again! Enter your answer:3 It is high. Try again! Enter your answer:1 It is low. Try again! The computer picked the number:2. You have lost the game now! Case-2: You have won the game. The computer has picked a random number for you. Clue: It is between 1 and 15 (both inclusive). Can you guess it within 3 attempts? Make a try. Enter your answer:10 It is low. Try again! Enter your answer:14 Excellent. You have guessed it right in 2 attempt(s). Author’s Comment: For a simple illustration, the computer picks the number between 1 and 15. The game will be more challenging when it picks the number from a broader range. Similarly, it becomes easy, if it picks the number from a narrow range. Once you see the solution, I encourage you to vary the range through user input. Chapter 5: Iteration using Loops In programming, iteration is used for repetition. When you write a program or implement an algorithm, you may need to iterate over a certain part of the code until a specific condition meets. Loop statements help you iterate over those portions of the code. In this chapter, you will learn about various kinds of loop statements. You can use them with conditional statements to make smart programs. The Purpose of Iteration The first question that may come into your mind is: why do I need loop statements (or, iterations)? To understand the answer, let’s write a simple program that can print 1, 2, and 3 in separate lines. print("Printing 1 to 3:") print(1) print(2) print(3) When you execute this code, you see the following output: Printing 1 to 3: 1 2 3 It is a simple program, and this output is obvious. You should not have any problem to understand this program. Now I give you a task: I want you to write a program which can print up to 5000 or 50000 . If you follow the previous approach, -how many print statements are required? Can you imagine this? Even if you use a simple copy/paste technique, there is a substantial amount of work. This is an impractical and tedious approach. Is there any alternative? Yes, you can exercise a better approach. Loop statements are ready to help you in similar situations. There are different loop statements. You can use them at your convenience. For now, let’s start with while loops. The while Loop The while loop continues executing as long as a certain condition is true. It has the following form: while “Your specified condition(s)”: statement-1 statement-2 statement-3 …. If you look closely at the previous syntax, you will notice the following points: It starts with a reserved keyword while There is a colon (:) after the condition(s) you mentioned. There can be many statements inside the while loop. You indent these lines. You check the condition before you enter this block of statements. If the condition is true, the control can enter the block and execute these statements. You repeat this block of statements till the condition is true. In short, to come out from a while loop, the condition needs to be false. Otherwise, the block of statements inside the loop will continue executing. When the condition becomes false, the block of statements inside while loop will no longer execute. Then we say that the control has exited from the while loop. It also means that if the condition is false at the beginning, you cannot enter the body of a while loop at all. If you enter a loop but do not satisfy the exit criteria, you fall into the trap of an infinite loop. Notice the body of the while loop. You can see that those are following correct indentation (same number of spaces from left). This correct indentation is important. Indentations tell you which statements are inside the while loop, and which are not. In the following example, I’m introducing a variable called current_number . In the beginning, I set its value to 1. Then I introduce a while statement. In this loop, I check a condition. The condition says that if the current_number is less than or equal to 10, I'll enter the loop. Then I print the current value of this variable, increment the value of the variable by 1, and repeat this process. The following flowchart can depict the scenario (See Figure 5-1): Figure 5-1: Flowchart of a while loop. Arbitrarily, I have chosen the number 10. But you can choose any number you want. Based on my conditional criteria, I can print 1 to 10 using this while loop. To make the program short and simple, I have used only one print statement outside the while loop. Let us follow demonstration 1 now. I’ve kept many comments for your easy understanding. Demonstration 1 I have created the file while_loop_ex_1.py . It has the following content: # Printing 1 to 10 using while loop print("Printing 1 to 10 using while loop:") current_value = 1 while current_value <= 10: print(current_value) # incrementing the value # Following line is a shortcut for : # current_value=Current_value+1 current_value += 1 # This statement is placed outside the while loop print("Job has been done. Exit from the while loop.") Output When you run this program, you’ll see the following output: Printing 1 to 10 using while loop: 1 2 3 4 5 6 7 8 9 10 Job has been done. Exit from the while loop. I have already mentioned that I have chosen the number 10 at random. So, if you want a print up to 500 or 5000 etc., you can change the condition inside while loop. For example, you can replace 10 with your intended value. Before you move into the next section, let me show you a program. This program causes an infinite loop. I’ve made demonstration2 for this purpose. It is as follows: Demonstration 2 Here is the complete program. # An incorrect implementation of while loop. # It creates an infinite loop. i=0 while i != 1: print(f"I cannot come out from the loop.i={i}.") Output When you run this program, you’ll see the program falls into the infinite loop. So, it keeps printing the following lines: I cannot come out from the loop.i=0. I cannot come out from the loop.i=0. I cannot come out from the loop.i=0. I cannot come out from the loop.i=0. I cannot come out from the loop.i=0. ………… ………… Analysis I hope that you understand the problem. It is because you have not implemented the proper logic. You need to change the value of i that was set to 0. But the control inside the while loop cannot come out unless you make i=1 . Therefore, you need to be careful enough when you implement your logic using a while loop. The for Loop Using while loops are common in programming. Notice the construction and usage of the while loop again. You can see that there are three major parts which are as follows: Initialization- You use it before the control enters the while loop Condition- You use this to determine whether the control enters the loop. If you enter the loop, the condition is further checked to determine whether the control will be inside the loop, or it will exit from the loop. Update-You have to implement the proper logic to update your intended variable (which you use in the condition of the while loop). This helps you to come out from the loop and process the next statement if any. If the control cannot exit from the loop, you fall into an infinite loop. You have seen an example of an infinite loop in Demonstration2. There is another loop called for loop in Python. In certain situations, this loop provides a convenient way to express the steps of a while loop. To understand it better, I make an equivalent program of Demonstration1. This program shows you the use of a for loop. Demonstration 3 Here is the complete program. # Printing 1 to 10 using for loop print("Printing 1 to 10 using a 'for' loop:") # I use the range(1,11) function to print # values 1 to 11-1=10 for current_value in range(1, 11): print(current_value) # This statement is placed outside the for loop print("Job has been done. Exit from the 'for' loop.") Output If you run this program, you get the following output: Printing 1 to 10 using a 'for' loop: 1 2 3 4 5 6 7 8 9 10 Job has been done. Exit from the 'for' loop. Analysis You can see that demonstration 3 and demonstration 1 generate the equivalent output. This time I have replaced the word ‘while’ with the word ‘for’ in the print statement. You have seen an example of a for loop in demonstration 3. Now I show you a few more use cases using this kind of loop. Let us continue reading. You often use the for loops when you traverse an iterable element like list, tuple, strings, etc. What is an iterable element? In simple terms, an iterable element is such an element that can be looped over. The general syntax to traverse and print the content of an iterable element is as follows: for element in iterable_element: print(element) In the next chapter, you’ll see the list data type in detail. To understand the upcoming example, it is enough for you to know that a list can store a sequence of elements. These elements may come from the same data type or different data types. You can define a list something like: list_name=[value1,value2,value3,..] Now assume you have a list of employee names as follows: employee_names = ["Sam", "Bob", "George", "Kate", "Julie"] Here, to print the employee names, you can use the following code segment: for employee in employee_names: print(employee) POINTS TO REMEMBER You may be new to programming and unaware of this format. So, you can pick each name from this list one-by-one and then print those names. It’s ok for a small set of names. But consider a scenario where your list contains many names, or when the list changes very often. In these cases, to access the list elements, the for loop can provide the best potential support to you. I have used the word ‘iterable’ previously. When you’ll learn more, you’ll know that an iterable object has a method called iter() . You can use it to get an iterator. For example, for the following list: employees = ["Sam", "Bob", "George"] I can print the values inside the list using the iter() and next() methods. To show this, I am using a Python command shell now. Look into these code fragments: >>> employees = ["Sam", "Bob", "George"] >>> iterator=iter(employees) >>> next(iterator) 'Sam' >>> next(iterator) 'Bob' >>> next(iterator) 'George' Demonstration4 provides a sample program for you and it is as follows: Demonstration 4 In this program, the first time when you pass through the for loop, the first element of the list, “Sam” is assigned to the variable employee . Next time, the second element in the list, “Bob” is assigned to the variable employee . And it continues like this until you reach the end of the list. Let us go through the complete program. # Printing employee names using for loop employee_names = ["Sam", "Bob", "George", "Kate", "Julie"] print("Printing employee names using a 'for' loop:") for employee in employee_names: print(employee) Output Run this program and see the following output. Printing employee names using a 'for' loop: Sam Bob George Kate Julie Analysis I want you to note that in demonstration 4; I have written: for employee in employee_names: print(employee) I have chosen the variable name as an employee . You are free to choose any valid name. For example, the following code can produce the same output: for i in employee_names: print(i) But you’ll probably agree with me that ‘ employee’ is a better choice than ‘i’ in this example. POINT TO REMEMBER We can summarize the difference between a for loop and a while loop as follows: A while loop runs as long as the specified condition is true. In contrast, a for loop is suitable for a collection of items where you want to execute a block of code for each item in the collection. In previous demonstrations, I have shown you the use of the range() function. The general form of this function is: range(begin, end, step) where You use the begin parameter for the first value. The default value is 0. You use the end parameter for the one past the last value You use the step parameter to denote the increment or decrement value. You can use the step parameter wisely. For example, the following code segment can print 0,3,6, and 9 . # Printing 0,3,6,9 using for loop for current_value in range(0, 11, 3): print(current_value) Notice that this code segment prints the values in the following order: 0 0+3=3 3+3=6 6+3=9 The next value 9+3=12 is beyond the limit/range (11-1=10). So, you do not see 12 in this output. You can use the for loop to print these values from the reverse direction. Here is the code segment for your reference: # Printing 0,3,6,9 using for loop for current_value in range(9, -1, -3): print(current_value) Notice that this code segment prints the values in the following order: 9 9-3=6 6-3=3 3-3=0 The next value 0-3=-3 is beyond the limit/range. So, you do not see -3 in this output. Use of break Statement You need to exit from a loop based on a certain condition(s). For example, suppose you have the following program: current_value = 1 while current_value <= 5: print(current_value) current_value += 1 You get the following output when you execute the program: 1 2 3 4 5 But let’s assume you do not want to go to the end of the loop. You decide that you’ll exit from this loop when the current_value becomes 3. Let me change the while block now. Now I insert another if block inside it. This if block has a break statement ( break is a keyword in Python) and the changed version is: while current_value <= 5: print(current_value) if (current_value == 3): print("Exit the loop when currnet_value becomes 3.") break # incrementing the value current_value += 1 If you execute this segment of code, you see the following output: 1 2 3 Exit the loop when current_value becomes 3. You can see that a break statement allows the loop to terminate prematurely. Now go through the following demonstration. It contains more comments for your easy understanding. Demonstration 5 Here is the complete program. print("***Using a break statement inside a while loop.***" ) current_value = 1 print(f"Initially, current_value={current_value}”) print("Exit the loop when current_value becomes 3.") while current_value < 5: # incrementing the value current_value += 1 print(f"The current value={current_value}") if current_value == 3: break print("I'm still inside the while loop.") # This statement is placed outside the while loop print("Job has been done. Exit from the while loop.") Output When you run this program, you’ll see the following as output: ***Using a break statement inside a while loop.*** Initially, current_value=1 Exit the loop when current_value becomes 3. The current value=2 I'm still inside the while loop. The current value=3 Job has been done. Exit from the while loop. Analysis Notice that the program does not print the statement “I'm still inside the while loop.” when the current_value becomes 3. It is because I place this statement after the break statement. Therefore, when a break statement executes, the control jumps out of the loop. POINT TO NOTE You may wonder why do I complicate this thing? You may suggest if I need to exit at 3, why should I continue till 5? So, I could change the condition of the while loop as follows: while current_value <= 3: #rest of the code Wait! This is a simple demo and the need to exit from the loop is not important. But consider a case when you generate some random numbers inside a loop. Here, you are not sure about the number in advance. Also, you may design the application in such a way that for a particular random number inside a loop, you do not process further and you exit from the loop. In cases like this, a break statement is useful. Use of continue Statement You have seen the use of a break statement. There is another interesting keyword in Python called continue . Using this keyword, you can skip some statements inside a loop in an iteration. You place these statements after the keyword continue . There is a reason for this. When a specific condition is met, you may not want to exit entirely from a loop. Instead, in this iteration, you may want to skip the remaining statements of the loop and go back to the beginning of the loop. The continue keyword is made for this purpose. To understand the usage better, I use a similar example which you see in Demonstration5. The only difference is: this time, I use continue instead of break . I also make some meaningful changes to print the messages. See the output and follow the analysis section. These help you understand the working mechanism easily. Let us follow demonstration 6. Demonstration 6 Here is the complete program. print("***Using a continue statement inside a while loop.***") current_value = 1 print(f"Initially, current_value={current_value}") print("I skip the code when current_value becomes 3.") while current_value < 5: # incrementing the value current_value += 1 print(f"The current value={current_value}") if current_value == 3: continue print("I'm still inside the while loop.") # This statement is placed outside the while loop print("Job has been done. Exit from the while loop.") Output When you run this program, you’ll see the following output: ***Using a continue statement inside a while loop.*** Initially, current_value=1 I skip the code when current_value becomes 3. The current value=2 I'm still inside the while loop. The current value=3 The current value=4 I'm still inside the while loop. The current value=5 I'm still inside the while loop. Job has been done. Exit from the while loop. Analysis Notice that the program does not print the statement “I'm still inside the while loop.” when current_value becomes 3. It is because I place this statement after the continue statement. In short, when the program executes the continue statement, it skips the remaining part of the loop for this iteration. Notice that you can see the print statement in other iterations. The loop also continues even after the continue statement. But in case of a break , the control jumps out from the loop. You have seen that behavior in demonstration 5. POINTS TO NOTE The concept of while loop, break, and continue statements are very similar when you program with C, C++, Java, or C#. Nested Loop Now I show you an example of a nested loop. In the previous chapter, you have seen that a program can contain an if statement inside another if statement. We call it a nested if statement. Similarly, you can have nested loops, which means you can have a loop that is contained in another loop. It is useful when you want to repeat an iterative process. Demonstration 7 The following is an example that has two for loops. For each iteration of the “outer for loop”, the “inner for loop” execute completely. See the following demonstration. print("*** Nested loop example.***") colors = ["Red","Green","Yellow"] fruits = ["Mango","Banana"] for color in colors: for fruit in fruits: print(color,fruit,end=" ") print() Output Here is the output: *** Nested loop example.*** Red Mango Red Banana Green Mango Green Banana Yellow Mango Yellow Banana Analysis You can see that I use a parameter called end inside the print() function. It becomes available in Python3. By default, the parameter is ‘ \n’ which is a newline character. You can pass any other character or string to it. For example, you can see that I pass some spaces in the previous code segment. I use this function in Chapter 9 as well. EXERCISE E5.1 Assume that there is an application where users supply some valid numbers only. It is because you do not need to write extra code to validate the input. Now write a python program where a user can keep supplying the numbers, and for each input, it can print whether it is a positive number or not. The program will end when the user types quit. E5.2 Create a list of numbers including integers and floats. Make sure that you have at least 5 numbers. a) Using a for loop, print the content of this list. b) Print the elements from index position 2 to 5. E5.3 Predict the output of the following code segment: for i in range(10, 20, 5): print(i) E5.4 Predict the output of the following code segment: for i in range(20,9,-5): print(i) E5.5 How many asterisks do you expect to see from the following code segment? for i in range(20,9,-5): print(i) Solution to Exercises E5.1 # Solution to Assignment 5.1 # Initially flag contains an empty string. # We need a string other than quit to enter # into the while loop to proceed further. flag = "" while flag != 'quit': user_input = input("Enter a valid number(Type quit to end the program): ") # If the user does not type 'quit' # we can convert the valid user input to float if user_input != 'quit': user_input = float(user_input) else: flag = 'quit' break; # exit from the loop if user_input > 0: print("You have supplied is a positive number.") elif user_input < 0: print("The supplied number is a negative number.") else: print("You've entered 0.") print("Thank you. It is the end of the program.") Here is a sample output: Enter a valid number(Type quit to end the program): 12 You have supplied is a positive number. Enter a valid number(Type quit to end the program): -35 The supplied number is a negative number. Enter a valid number(Type quit to end the program): 12.5 You have supplied is a positive number. Enter a valid number(Type quit to end the program): quit Thank you. It is the end of the program. Author’s Comment: You may think: Can I use a number (an integer, or a float) flag to exit from the program? I suggest you not to do so. It is because, in that case, your program can suffer from a potential drawback. When you’ll learn about antipatterns, you’ll know that there is an anti-pattern called Zero Means Null . The proposed program can suffer from it if you use a number flag. For example, let’s assume that when a user press -999, you’ll end the program. But if you treat -999 to exit from the loop, what will you do if you need this number? So, you cannot treat -999 like any other number now. Therefore, experts suggest that we choose the exit criterion wisely. E5.2 # Solution to Assignment 5.2 # A list of numbers number_list = [1, 2.2, 4.4, 5, 6, 9.3, 102] print("Printing the numbers inside number_list using a 'for' loop:") for number in number_list: print(number) print("Now printing the numbers which have the index between 2 and 5:") for index in range(2, 6): print(number_list[index]) Output: Here is the output: Printing the numbers inside number_list using a 'for' loop: 1 2.2 4.4 5 6 9.3 102 Now printing the numbers which have the index between 2 and 5: 4.4 5 6 9.3 E5.3 10 15 Author’s Comment: The next value 15+5=20 falls outside the upper limit (20-1=19) E5.4 20 15 10 E5.5 4 Author’s Comment: These four asterisks appear due to the ‘ i’ values: 10, 5, 0 and -5 CS 3.1 Implementation I make this implementation using the randint() function, if-elif-else chain, and while loop. You have seen randint() in Chapter 3, if-elif-else chain in Chapter 4, and while loop in Chapter 5. Refer to the supporting comments and strings that I use in print() functions. These help you understand the code. Here is the complete implementation for you. from random import randint picked_number = randint(1, 15) print("The computer has picked a random number for you.") print("Clue: It is between 1 and 15 (both inclusive).") print("Can you guess it within 3 attempts? Make a try.") no_of_attempt = 0 guess = False user_input = 0 # an initial value while no_of_attempt < 3: #User's input user_input = int(input("Enter your answer:")) no_of_attempt += 1 if user_input == picked_number: guess = True break elif user_input > picked_number: print("It is high. Try again!") else: print("It is low. Try again!") if guess: print("Excellent. You have guessed it right.") print(f"you've taken {no_of_attempt} attempt(s).") else: print(f"The computer picked the number:{picked_number}.") print("You have lost the game now!") Case Study IV CS 4.1 Problem Statement Let us design a multiple-choice question bank. Each question has four options. The user needs to pick the correct answer among these options. Once the test is over, he can see the score. For a better understanding, I supply some sample output for you: Welcome to the MCQ test. ========================= Q1.What is the value of the expression:2*3-4? (a)1 (b)2 (c)3 (d)None. Type your answer(a/b/c/d):b Q2.What is the value of the expression:1+(2*3)/4? (a)1.5 (b)3 (c)3.5 (d)None. Type your answer(a/b/c/d):d Q3.The set data type can hold duplicate values. The statement is: (a)False (b)True (c)Partially correct. (d)None. Type your answer(a/b/c/d):a Your Score:2 out of 3 Author’s Comment: Once you get the idea, you can add more questions to this test. The test is more challenging when you do not pick a fixed set of questions. Instead, you can select a specific number of questions at random from an extensive set of questions. At this stage, you can work with a fixed number of questions only. Chapter 6: Advanced Data Types This chapter shows you the use of some advanced data types which are very common in Python programming. We’ll cover them quickly in the upcoming sections. Working with Lists Lists are used to store a sequence of elements. In a list, you can have just a few items or millions of items. Here you may see the presence of mixed data types too. Formally, you can say a list can contain a sequence of objects which may come from different data types. You define a list something like the following: list_name=[value1,value2,value3,..] You can see that I am separating the values inside the square bracket [ ]. This kind of arrangement can help you to store multiple values into a single variable. For example, in the following code segment, I store three different names(strings) inside a list, called mylist1. Then I print the contents of the list. # A list with three strings my_list1 = ["John", "Bob", "Sam"] print("The my_list1 is as follows:") print(my_list1) When you run this code segment, you can get the following output: The my_list1 is as follows: ['John', 'Bob', 'Sam'] Similarly, you can have another code segment, where you use another list, called my_list2 to hold the value of 5 numbers and print the contents as follows: # A list with five numbers my_list2 = [1, 2, 3.7, 4, 5.2] print("The my_list2 is as follows:") print(my_list2) When you run this code segment, you can get the following output: The my_list2 is as follows: [1, 2, 3.7, 4, 5.2] You may notice that I use the list names as my_list1,my_list2, etc. In real-world programming, you may see that developers use “plural names” such as names, numbers, etc. It is because a list usually contains multiple elements. You can also declare an empty list and later add elements using a special function called append(). But wait! Before you read further, let me quickly show some of the common use cases of lists. I’m keeping the comments for your easier understanding. Lastly, like the previous chapter, instead of creating separate files for each of these small code segments, I have put them into a single file. But it’s a choice. So, if you want, you can always write a new program in a new file and execute it. Now go through the following code fragments: Goal: To show you that a list can store mixed data types. Code: # A list can store mixed data types my_list = ["John", 12, "Sam", True, 50.7] print(my_list) print("----------------") Expected Output: ['John', 12, 'Sam', True, 50.7] Goal: Printing the elements of a list using an index. The indexing starts with 0 from the extreme left. Code: # You can use a list index. The usage is similar to strings. my_list = ["John", 12, "Sam", True, 50.7] print(my_list[0]) # John print(my_list[1]) # 12 print(my_list[2]) # Sam print(my_list[3]) # True print(my_list[4]) # 50.7 # Error: List index out of range # print(my_list[5]) Expected Output: John 12 Sam True 50.7 Explanation: Notice that the line print(my_list[5]) is commented. There is currently no element at index position 5 in my_list . So, you’ll receive an error if you try to use mylist[5] now. This error is similar to the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter4/list_usages_file_1.py", line 14, in <module> print(mylist[5]) IndexError: list index out of range Note: I am placing several code segments in the same file. So, in the previous segment, line number 14 indicates the error location in my file, which may differ in your case. The same comment applies to similar error messages in this book. Goal: Printing the elements of a list from the extreme right. In this case, indexing starts from -1. Code: # You can use list indexing from the right end. # In this case, it starts from -1 my_list = ["John", 12, "Sam", True, 50.7] print(my_list[-1]) # 50.7 print(my_list[-2]) # True print(my_list[-3]) # Sam print(my_list[-4]) # 12 print(my_list[-5]) # John # Error: List index out of range # print(my_list[-6]) Expected Output: 50.7 True Sam 12 John Explanation: Notice that the line print(mylist[-6]) is commented in the code segment. There is currently no element at index position -6 in my_list . So, you’ll receive an error if you try to use mylist[-6] now. Similar to the previous case, the error may appear as follows: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter4/list_usages_file_1.py", line 25, in <module> print(mylist[-6]) IndexError: list index out of range Goal: I want to print only a specific portion of the list. Refer to the supportive comments. Code: # Printing list elements starting from a particular position my_list = ["John", 12, "Sam", True, 50.7] print("The original list is as follows:") print(my_list) print("Printing the elements starting from index position 2 to end:") print(my_list[2:]) print("Printing the elements starting from index position 1 to 3(i.e.4-1):") print(my_list[1:4]) Expected Output: The original list is as follows: ['John', 12, 'Sam', True, 50.7] Printing the elements starting from index position 2 to end: ['Sam', True, 50.7] Printing the elements starting from index position 1 to 3(i.e.4-1): [12, 'Sam', True] Explanation: When you use mylist[2:], you consider elements starting from index 2 to the end of the list. When you use mylist[1:4], you consider elements starting from index 1 to index 4-1 , i.e. 3 . Goal: You can reassign a new value in the list. Here is an example. Code: # You can reassign a new value inside the list my_list = ["John", 12, "Sam", True, 50.7] print("The original list is as follows:") print(my_list) print("Changing the element at index 2.") my_list[2] = "Bob" print("Now the list is as follows:") print(my_list) Expected Output: The original list is as follows: ['John', 12, 'Sam', True, 50.7] Changing the element at index 2. Now the list is as follows: ['John', 12, 'Bob', True, 50.7] Explanation: You can see that I’ve set a new value at index position 2. So, instead of ‘Sam’ , you see ‘Bob’ inside this modified list. Goal: I want to concatenate multiple strings. There are various ways to accomplish this task. The simplest among them is to use the “+” operator. One sample usage of this is shown in the following example when I concatenate two lists called my_list1 and my_list2 . Code: # Concatenation example my_list1 = ["John", 12, 50.7] my_list2 = ["Sam", 25, "John", False, 100.2] print("Original lists are :") print(my_list1) print(my_list2) print("After concatenating the lists, you get the following list:") print(my_list1 + my_list2) Expected Output: Original lists are : ['John', 12, 50.7] ['Sam', 25, 'John', False, 100.2] After concatenating the lists, you get the following list: ['John', 12, 50.7, 'Sam', 25, 'John', False, 100.2] Explanation: Now you see an example to concatenate multiple lists. This is very common in Python programming. The concatenated list can have duplicates; for example, once you concatenate the lists, notice that ‘ John’ from both lists are present in the new list. Goal: I want to print a specific number of elements from a list. To demonstrate this, in this example, I print the last 3 elements, the last 2 elements, and the last element of a list. Code: # Printing a specific number of elements of a list from the # end. my_list = ["John", 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("The last 3 elements of the list are:") print(my_list[-3:]) print("The last 2 elements of the list are:") print(my_list[-2:]) print("The last element of the list is:") print(my_list[-1]) Expected Output: The original list is: ['John', 12, 'Sam', True, 50.7] The last 3 elements of the list are: ['Sam', True, 50.7] The last 2 elements of the list are: [True, 50.7] The last element of the list is: 50.7 Explanation: This segment gives you the idea: how to print a specific element or a specific portion of elements inside a list (starting from the end location). Goal: I remove an element from a list. Here I use the del() function. At first, I remove the element from index position 2. In the next step, I again remove another element from the modified list. This time I remove the element from index location 3. Code: # Removing an element using del() my_list = ["John", 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Removing the element at index 2.") del (my_list[2]) print("Now the list is:") print(my_list) print("Removing the element at index 3 from this updated list.") del (my_list[3]) print("The updated list is:") print(my_list) Expected Output: The original list is: ['John', 12, 'Sam', True, 50.7] Removing the element at index 2. Now the list is: ['John', 12, True, 50.7] Removing the element at index 3 from this updated list. The updated list is: ['John', 12, True] Explanation: This example shows the repetitive use of the del() function to modify a list. Goal: Removing an element from a list. Here I use remove() function. Code: # Removing an element using remove()function my_list = ["John", 12, 25, 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Removing the first occurrence of 12 inside the list.") my_list.remove(12) print("Now the list is:") print(my_list) Expected Output: The original list is: ['John', 12, 25, 12, 'Sam', True, 50.7] Removing the first occurrence of 12 inside the list. Now the list is: ['John', 25, 12, 'Sam', True, 50.7] Explanation: Initially, there were two 12 inside the list. The remove() function has removed the first occurrence of 12. It was present before element 25. Notice that another 12 is still present in this modified list. Goal: In Python, elements are case-sensitive. For example, in the following code, the remove() function can correctly remove the ‘Sam’ but not the ‘sam’ which appeared before ‘Sam’ . Code: # Elements are case-sensitive my_list = ["John", 12, "sam", 25.7, "Sam", True] print("The original list is:") print(my_list) print("Removing the first occurrence of 'Sam' inside the list.") my_list.remove('Sam') print("Now the list is:") print(my_list) Expected Output: The original list is: ['John', 12, 'sam', 25.7, 'Sam', True] Removing the first occurrence of 'Sam' inside the list. Now the list is: ['John', 12, 'sam', 25.7, True] Goal: Removing an implement from a list using pop() . Inside this function, you need to supply the index location. Code: # Removing an element using pop() my_list = ["John", 12, 25, 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Removing an element inside the list at index 3 using pop().") my_list.pop(3) print("Now the list is:") print(my_list) Expected Output: The original list is: ['John', 12, 25, 12, 'Sam', True, 50.7] Removing an element inside the list at index 3 using pop(). Now the list is: ['John', 12, 25, 'Sam', True, 50.7] Explanation: Initially, my_list contained two 12. The first 12 was at index location 1 and the second 12 was at index location 3. So, the following line of code: my_list.pop(3) removes the second occurrence of 12 from the original list. Goal: You want to examine whether a particular element is currently present inside a list. Code: # Checking whether an element is present inside a list my_list = ["John", "Bob", "Sam", "Ester", 1, 2, 3, 4] print("Is 'Sam' present inside the list?") print('Sam' in my_list) # True print("Is 'Jeniffer' present inside the list?") print('Jennifer' in my_list) # False print("Is 3 present inside the list?") print(3 in my_list) # True print("Is 5 present inside the list?") print(5 in my_list) # False # Checking whether an element is absent inside a list print("Is 'Jeniffer' NOT present inside the list?") print('Jennifer' not in my_list) # True Expected Output: Is 'Sam' present inside the list? True Is 'Jeniffer' present inside the list? False Is 3 present inside the list? True Is 5 present inside the list? False Is 'Jeniffer' NOT present inside the list? True Explanation: The output message is self-explanatory. You can note that“ not in” performs the reverse check. Notice that the following code segment: print('Jennifer' not in my_list) # True outputs the following: True Goal: I want to find the maximum element and minimum element from a list. Code: # Finding the maximum and minimum from a list # This list contains the numbers only my_list = [1, 23, 56.2, -3.7, 999] print("The original list is:") print(my_list) print(f"The largest number is:{max(my_list)}") # 999 print(f"The smallest number is:{min(my_list)}") # -3.7 print("----------------") Expected Output: The original list is: [1, 23, 56.2, -3.7, 999] The largest number is:999 The smallest number is:-3.7 Note: The max() and min() functions can work if the list contains numbers only; otherwise, you’ll encounter errors. For example, the following code segment: #For this segment, you'll receive an error my_list = [1, 23, 56.2, -3.7, 999, "abc", "bob"] print("The original list is:") print(my_list) print(f"Largest number is :{max(my_list)}") print(f"Smallest number is :{min(my_list)}") Can generate the following error: The original list is : [1, 23, 56.2, -3.7, 999, 'abc', 'bob'] Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter4/list_usage_file_2.py", line 15, in <module> print(f"Largest number is :{max(my_list)}") TypeError: '>' not supported between instances of 'str' and 'int' Goal: Testing max(), min() on boolean values. Code: # Testing booleans with max() and min() my_list = [0.75, True, False, 0.5, 0.6, 1, 0] print("The original list is:") print(my_list) print(f"The largest number is:{max(my_list)}") # True print(f"The smallest number is:{min(my_list)}") # False Expected Output: The original list is: [0.75, True, False, 0.5, 0.6, 1, 0] The largest number is:True The smallest number is:False Explanation: By design, if you use these Boolean values in numerical context, there is no error; True is treated as 1 and False is treaded as 0. So, in this case, you can see the largest number is True and the smallest number is False . To test this fact, in PyCharm, I rearrange the list. Notice that I have interchanged the positions of True and 1 in this list. Also, I have interchanged the position of False and 0 here. # Testing booleans with max() and min() my_list = [0.75, 1, 0, 0.5, 0.6, True, False] print("The original list is:") print(my_list) print(f"The largest number is:{max(my_list)}") # 1 print(f"The smallest number is:{min(my_list)}") # 0 Now run this modified segment. You can see the following output: The original list is as follows [0.75, 1, 0, 0.5, 0.6, True, False] The largest number is:1 The smallest number is:0 The online link https://docs.python.org/3/library/stdtypes.html#booleanvalues from python doc confirms this saying: “Boolean values are the two constant objects False and True. They are used to represent truth values (although other values can also be considered false or true). In numeric contexts (for example when used as the argument to an arithmetic operator), they behave like the integers 0 and 1, respectively. In numeric contexts (for example when used as the argument to an arithmetic operator), they behave like the integers 0 and 1.” Goal: I want to add one element at the end of a list. I use the append() function for this purpose. Initially, I append 25 at the end of the list. Later I append another element “ Bob ” to the modified list. Code: # I want to add an element at the end of a list my_list = ["John", 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Appending 25 at the end of the list.") my_list.append(25) print("Now the list is:") print(my_list) print("Appending another element 'Bob' now.") my_list.append("Bob") print("The modified list:") print(my_list) Expected Output: The original list is: ['John', 12, 'Sam', True, 50.7] Appending 25 at the end of the list. Now the list is: ['John', 12, 'Sam', True, 50.7, 25] Appending another element 'Bob' now. The modified list: ['John', 12, 'Sam', True, 50.7, 25, 'Bob'] Note: Using append (), you can add one element at a time. If you try to append multiple elements like the following: my_list.append(10,20) You will receive the error similar to the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter4/list_usage_file_2.py", line 52, in <module> my_list.append(10,20) #error TypeError: append() takes exactly one argument (2 given) Goal: Using append(), you can add a single element only. But you can use this function to add a list that contains multiple elements. Let us see how it looks. Code: my_list = ["John", 12, "Sam", True, 50.7] print("The initial list is:") print(my_list) print("Appending [10,'Bob',100.2] at the end of list:") my_list.append([10, 'Bob', 100.2]) print("The modified list:") print(my_list) Expected Output: The initial list is: ['John', 12, 'Sam', True, 50.7] Appending [10,'Bob',100.2] at the end of the list: The modified list: ['John', 12, 'Sam', True, 50.7, [10, 'Bob', 100.2]] Explanation: Notice that in this modified list, the final element is again a list. And it is NOT like other elements in my_list . Goal: You can add multiple elements to a list using the extend() function. Here I show you an example. Code: # You can add multiple elements to a list # using extend() function. # Here is an example. my_list = ["John", 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Adding 10,'Bob', and 100.2 at the list end.") my_list.extend([10, 'Bob', 100.2]) print("Now the list is:") print(my_list) Expected Output: The original list is: ['John', 12, 'Sam', True, 50.7] Adding 10,'Bob', and 100.2 at the list end. Now the list is: ['John', 12, 'Sam', True, 50.7, 10, 'Bob', 100.2] Additional note: You can compare the result with the previous example when you used the append() function. You can see that when you use append() , you got the modified list as ['John', 12, 'Sam', True, 50.7, [10, 'Bob', 100.2]] But using extend() , you have received the following output: ['John', 12, 'Sam', True, 50.7, 10, 'Bob', 100.2] Goal: You saw that you can add the elements at the end of a list. But you have the option to add an element in a particular position. The insert() function is useful in this case. In the following example, I insert an element “Jack” into a list at index location 3. Code: my_list = ["John", 12, "Sam", True, 50.7] print("The original list is:") print(my_list) print("Adding the element 'Jack' at index 3.") my_list.insert(3, "Jack") print("Now the list is as follows:") print(my_list) Expected Output: The original list is: ['John', 12, 'Sam', True, 50.7] Adding the element 'Jack' at index 3. Now the list is as follows: ['John', 12, 'Sam', 'Jack', True, 50.7] Goal: You can sort your list that contains the same datatype. Here is an example. Code: my_list = [33, 11, 555, 77, 111, 333] print("The initial list is:") print(my_list) print("Using sort() on my_list now.") my_list.sort() print("Now the list is:") print(my_list) Expected Output: The initial list is: [33, 11, 555, 77, 111, 333] Using sort() on my_list now. Now the list is: [11, 33, 77, 111, 333, 555] Note: Two points to remember when you use the sort() function: It changes the original list. If you want to avoid modifying the original list, you can refer sorted() function which I discuss next. If you have mixed data types in your list and you apply sort() on that, you’ll encounter errors. Here is an example, for the following code segment: my_list = ["John", 12, "Sam", True, 50.7] my_list.sort()#error now you’ll see the following error: TypeError: '<' not supported between instances of 'int' and 'str' Goal: As said before, if you want to prevent modification of the original list, you can use the sorted() function. Here I show you an example. Code: my_list = [33, 11, 555, 77, 111, 333] #my_list = ["sam","bob","jack"] print("Initially, my_list is:") print(my_list) print("Printing the sorted list now.") print(sorted(my_list)) print("The my_list now:") print(my_list) Expected Output: Initially, my_list is: [33, 11, 555, 77, 111, 333] Printing the sorted list now. [11, 33, 77, 111, 333, 555] The my_list now: [33, 11, 555, 77, 111, 333] Additional note: Keep in mind that you should use this function on the same datatypes. Working with Tuples Tuples are another important data type and similar to lists. But there are some noticeable differences which are as follows: Tuples are immutable. This means once created, you cannot incorporate changes in them. But you have seen that lists can be modified. For example, you reassigned a value in a list, you extended a list, etc. So, lists are mutable. When you create a tuple, you use codes something like the following (here I have chosen the variable name as my_tuple ): my_tuple = ("John", 12, "Sam", True, 50.7) you can see the declaring a tuple is similar to a list, but this time, I put elements inside the round brackets - (, ) All the functions that are available for lists are not available for tuples. For example, if you write the following: my_tuple = ("John", 12, "Sam", True, 50.7) my_tuple.remove(12) #error You can notice the error saying: AttributeError: 'tuple' object has no attribute 'remove' Or, if you write the following: my_tuple.append("Jack") #error You can notice the error saying: AttributeError: 'tuple' object has no attribute 'append' But there are some similarities with lists too. You can also convert a list to a tuple. So, let’s examine some built-in functions for tuples now. Goal: Declaring a tuple and printing the elements inside it. Code: my_tuple = ("John", 12, "Sam", True, 50.7) print("The content of my_tuple is:") print(my_tuple) Expected Output: The content of my_tuple is: ('John', 12, 'Sam', True, 50.7) Goal: I want to access the tuple elements. Code: # Indexing is similar to lists my_tuple = ("John", 12, "Sam", True, 50.7) print("The content of my_tuple is:") print(my_tuple) # Printing the first element print("The first element is:") print(my_tuple[0]) print("The last element is:") print(my_tuple[-1]) print("Printing the elements from index 1 to index 3.") print(my_tuple[1:4]) print("Printing the elements from index 2 to end.") print(my_tuple[2:]) Expected Output: The content of my_tuple is: ('John', 12, 'Sam', True, 50.7) The first element is: John The last element is: 50.7 Printing the elements from index 1 to index 3. (12, 'Sam', True) Printing the elements from index 2 to end. ('Sam', True, 50.7) Goal: I show you that you cannot reassign the value inside a tuple. Code: # You cannot reassign the value inside a tuple. my_tuple = ("John", 12, "Sam", True, 50.7) print("The content of my_tuple is:") print(my_tuple) print("Trying to replace 'Sam' with 'Bob':") my_tuple[2]= 'Bob' #error Expected Output: You are supposed to see the error similar to the following: The content of my_tuple is: ('John', 12, 'Sam', True, 50.7) Trying to replace 'Sam' with 'Bob': Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter4/tuple_usage_file.py", line 29, in <module> my_tuple[2]= 'Bob' #error TypeError: 'tuple' object does not support item assignment Explanation: Tuples are immutable by design. So, you cannot modify them. Goal: Converting a list to a tuple. Code: my_list = ["John", 12, "Sam", True, 50.7] print("The content of my_list is:") print(my_list) # Converting the list to a tuple my_tuple=tuple(my_list) print("The content of my_tuple is:") print(my_tuple) Expected Output: The content of my_list is: ['John', 12, 'Sam', True, 50.7] The content of my_tuple is: ('John', 12, 'Sam', True, 50.7) Explanation: You can use the built-in tuple() function to convert a list to a tuple. Goal: Reversing a tuple. Code: my_tuple = (1, 2, 3, 4, 5) print("The content of my_tuple is:") print(my_tuple) print("Reversing the tuple:") rev_tuple = tuple(reversed(my_tuple)) print("The content of rev_tuple is:") print(rev_tuple) Expected Output: The content of my_tuple is: (1, 2, 3, 4, 5) Reversing the tuple: The content of rev_tuple is: (5, 4, 3, 2, 1) Working with Dictionaries Now you see the use of another important data type. You call it a dictionary . These are some noticeable characteristics of this datatype: It is a key-value pair. The keys are unique. Keys and values can be of any type. Dictionaries are indexed through its keys. When you create a dictionary, you use codes something like the following (here I have chosen the variable name as my_dictionary ): my_dictionary = {1: "John", 2: 12, 3: "Sam", 4: True, 5: 50.7} you can see that I put elements inside the curly brackets –{ , } now. Let us examine some built-in functions for dictionaries which are as follows. Goal: I want to create a dictionary and print the details inside it. Code: # A dictionary with 5 key-value pair my_dictionary = {1: "John", 2: 12, 3: "Sam", 4: True, 5: 50.7} print("The my_dictionary contains:") print(my_dictionary) Expected Output: The my_dictionary contains: {1: 'John', 2: 12, 3: 'Sam', 4: True, 5: 50.7} Goal: I want to use different types of keys in my dictionary. In the following code segment, you’ll see that first three keys are numbers and remaining keys are strings. Code: # A dictionary with 5 key-value pair # Choosing different types of keys in the same dictionary my_dictionary = {1: "John", 2: 12, 3: "Sam", 'fourth': True, 'fifth': 50.7} print("The my_dictionary contains:") print(my_dictionary) print("----------------") Expected Output: The my_dictionary contains: {1: 'John', 2: 12, 3: 'Sam', 'fourth': True, 'fifth': 50.7} Goal: I want to print values for particular keys in a dictionary. Code: # I want to print values for particular keys my_dictionary = {1: "John", 2: 12, 3: "Sam", 'fourth': True, 'fifth': 50.7} print("The my_dictionary contains:") print(my_dictionary) print("Value at key 1:", my_dictionary[1]) print("Value at key 2:", my_dictionary[2]) print("Value at key 3:", my_dictionary[3]) print("Value at key 'fourth':", my_dictionary['fourth']) print("Value at key 'fifth'::", my_dictionary['fifth']) Expected Output: The my_dictionary contains: {1: 'John', 2: 12, 3: 'Sam', 'fourth': True, 'fifth': 50.7} Value at key 1: John Value at key 2: 12 Value at key 3: Sam Value at key 'fourth': True Value at key 'fifth':: 50.7 Goal: I assign different values for the same key in a dictionary and want to see the effect. I also want to check -how many elements are present in the dictionary? Code: # If you assign different values for the same keys # last assigned value will be kept my_dictionary = {1: "John", 2: "Sam", 3: "Jack",1: "Bob"} print("The my_dictionary contains:") print(my_dictionary) print("Value at key 1:", my_dictionary[1]) print(f"Number of contents:{len(my_dictionary)}") Expected Output: The my_dictionary contains: {1: 'Bob', 2: 'Sam', 3: 'Jack'} Value at key 1: Bob Number of contents:3 Explanation: It’s important to note that in a case like this, the dictionary will keep the last assigned value. For example, initially, I assigned John to key 1 , but later I assigned Bob to key 1 . So, when I print the details of the dictionary, you can see that the dictionary holds the last assigned value Bob for key 1 . The last line in the output shows that you can use the len() function to determine the total number of elements in your dictionary. Working with Sets Sets are a special kind of datatype that does not hold duplicate values. You can think of it as a combination of a list and a dictionary. For example, like dictionaries, you use curly brackets { and } but like lists, you do not have any key-value pair. The following is an example of a set: my_set1 = {1, 2, 3, "Jack", "Bob"} Alternatively, to create a set, you can use the built-in set() function like the following: myset=set(iterable_element) Where “ iterable_element “indicates that you can use something like a list, or a tuple like the following: #Alternative way to create a set #Using a list now to create a set my_set = set([1, 2, 3, "Jack", "Bob"]) #Using a tuple to create a set my_set = set((1, 2, 3, "Jack", "Bob")) Now go through the following code fragments to get some idea about set data types. Goal: I supply duplicate values to sets. Then I test whether these sets contain duplicates. Code: my_set1 = {1, 2, 3, "Jack", 2, "Bob", 3, 1} print("The my_set1 contains:") print(my_set1) my_set2 = {"Sam", "Bob", "Jack", "Sam", "Jack", "Ester"} print("The my_set2 contains:") print(my_set2) Expected Output: The my_set1 contains: {1, 2, 3, 'Jack', 'Bob'} The my_set2 contains: {'Sam', 'Jack', 'Bob', 'Ester'} Explanation: You can see that sets can’t have duplicates. Goal: Sets are mutable. In the following example, I’m adding 6 to the list and then removing 2 from the set. Code: # Sets are mutable my_set = {1, 2, 3, 4, 5} #my_set = {}#it is treated as empty dictionary #my_set=set()#this is ok for an empty set print("The my_set contains:") print(my_set) print("Adding 6 to the set now.") my_set.add(6) print("Now the set is:") print(my_set) print("Removing 2 from the set now.") my_set.remove(2) print("Now the my_set is:") print(my_set) Expected Output: The my_set contains: {1, 2, 3, 4, 5} Adding 6 to the set now. Now the set is: {1, 2, 3, 4, 5, 6} Removing 2 from the set now. Now, the my_set is: {1, 3, 4, 5, 6} Explanation: Here is an important point to note: if you just use the following: my_set = {}#it is treated as empty dictionary Python considers my_set as an empty dictionary, not an empty set. So, in this case, If you use the following code fragment: my_set = {} my_set.add(6) you’ll see the following error: AttributeError: 'dict' object has no attribute 'add' So, what is the remedy? You can opt for an alternative way to create a set. For example, the following line of code can create an empty set. my_set=set()#this is ok for an empty set and now the following lines of codes will work fine for you: print("The set,called 'my_set' is as follows:") print(my_set) print("Adding 6 to the set now.") my_set.add(6) Goal: You can iterate over strings. So, you can pass a string argument to the set() function. Code: # Strings are iterable. So, you can use strings to set(). my_str = "vaskaran" my_set = set(my_str) print("The my_set contains:") print(my_set) Expected Output: The my_set contains: {'n', 's', 'k', 'a', 'v', 'r'} Additional note: You can see that my_set is unordered and it does not contain the duplicates. Goal: In the previous code segment, you saw that sets are unordered. Now you see that you cannot access the set elements referring to an index. Here is an example. Code: # Sets are unordered. You cannot access the elements by referring to an index my_set = set([2,"abc",1,5]) print("The my_set contains:") print(my_set) print("Trying to access the 0th element.") print(my_set[0])#error Expected Output: The my_set contains: {1, 2, 5, 'abc'} Trying to access the 0th element. Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter4/set_usage_file.py", line 59, in <module> print(my_set[0])#error TypeError: 'set' object is not subscriptable Goal: You cannot access set elements by referring to an index. But You can loop through the elements. Here is an example. Code: # You cannot access set elements by referring to an index. # But You can loop through the elements. my_set = set([2, "hello", 1, 5]) print("The my_set contains:") for item in my_set: print(item) Expected Output: The my_set contains: {1, 2, 'hello', 5} Trying to access the 0th element. The my_set contains: 1 2 hello 5 Goal: Removing an element from a set. Here I show you the use of both remove and discard. The difference is: discard() does not raise an error if the element is not present, but remove() raises an error in that case. Code: # Remove an element from a set my_set = set([1,2,3,4,5]) print("The my_set contains:") print(my_set) print("Removing 5 using remove().") my_set.remove(5) print("Now the my_set contains:") print(my_set) print("Removing 4 now using discard().") my_set.discard(4) print("Now the my_set contains:") print(my_set) Expected Output: The my_set contains: {1, 2, 3, 4, 5} Removing 5 using remove(). Now the my_set contains: {1, 2, 3, 4} Removing 4 now using discard(). Now the my_set contains: {1, 2, 3} Note: Append the following code: # Trying to remove 4 when it is absent in the set. my_set.discard(4) # no error is raised my_set.remove(4) # error is raised If you execute the code, you see that remove() raises an error because 4 is not present in my_set now. The error is similar to the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter4/set_usage_file.py", line 82, in <module> my_set.remove(4) # error is raised KeyError: 4 But the discard() does not raise any error for this. EXERCISE E6.1 Create a list that contains more than 2 elements. Then remove the last two elements from the list. E6.2 Create a tuple with more than 5 elements. Then print the 3rd element of it. Can you print the 3rd element from last? E6.3 Create a tuple with elements 1,2,2,3,4,4,4,5.Then reverse the tuple and print how many times 4 appears in this tuple. E6.4 How a tuple is different from a list? E6.5 Predict the output: my_tuple = (1, 2, 2, 3, 4, 4, 5) my_set = set(my_tuple) print("The my_set is:") print(my_set) E6.6 Can you get any error for the following code? my_list=["red", "blue"] my_set = set(my_list) print("The my_set is:") print(my_set) my_set.discard("green") E6.7 Create a dictionary and print the details. Can you print the value for a particular key? Solution to Exercises E6.1 my_list = ["John", 12, 25, 12,"Sam", True, 50.7] print("The original list is:") print(my_list) #Removing the last two elements from the list del(my_list[-2:]) print("Now the list is:") print(my_list) Output: The original list is: ['John', 12, 25, 12, 'Sam', True, 50.7] Now the list is: ['John', 12, 25, 12, 'Sam'] E6.2 my_tuple = ("John", 12, 25, 12, "Sam", True, 50.7) print("The original tuple is:") print(my_tuple) #Printing the 3 element print("The third element:") print(my_tuple[2]) print("The third element from last:") print(my_tuple[-3]) E6.3 my_tuple = (1, 2, 2, 3, 4, 4, 4, 5) print("The original tuple is:") print(my_tuple) print("The reversed tuple is:") rev_tuple = tuple(reversed(my_tuple)) print(rev_tuple) print(f"The number of 4 in this tuple:{rev_tuple.count(4)}") E6.4 Tuples are immutable but lists are mutable. E6.5 The my_set is: {1, 2, 3, 4, 5} Explanation: Sets do not contain duplicates. E6.6 The element “green” is not present in the set. If you use remove() , you see a KeyError , but discard() does not raise any such error. The discard() method removes the element if it is present in the set. E6.7 Do it yourself. I have shown you code samples in this chapter. CS 4.1 Implementation I use a dictionary to store answers to the questions. I also use the len() function to get the total number of questions in this question bank. To beautify the output, I print the questions and corresponding options in the new lines. Here is the complete implementation for you. # All questions question1 = "Q1.What is the value of the expression:2*3-4?" \ "\n(a)1" \ "\n(b)2" \ "\n(c)3" \ "\n(d)None." question2 = "\nQ2.What is the value of the expression:1+(2*3)/4?" \ "\n(a)1.5" \ "\n(b)3" \ "\n(c)3.5" \ "\n(d)None." question3 = "\nQ3.The set data type can hold duplicate values." \ "The statement is:" \ "\n(a)False" \ "\n(b)True" \ "\n(c)Partially correct." \ "\n(d)None." # Storing the questions with answer keys # inside the following dictionary. question_bank = {question1: "b", question2: "c", question3: "a"} print("Welcome to the MCQ test.") print("="*25) score = 0 # initial value for key in question_bank: print(key) user_input = input("Type your answer(a/b/c/d):") if user_input == question_bank[key]: score += 1 print(f"\nYour Score:{score} out of {len(question_bank)}") Possible improvements Use a function to make a better implementation. In chapter7, you’ll learn about functions. At the end of Chapter 7, you’ll see a better solution. You can store more questions and pick a subset of these questions at random. Case Study V CS 5.1 Problem Statement This time you make a calculator. Using the calculator, you should able to perform the basic operations-addition, subtraction, multiplication, and division. Here I provide you a sample output when the user supplies the valid inputs: ========================= This is a simple calculator. It supports the following operations: i)Addition ii)Subtraction iii)Multiplication and iv)Division. ========================= Enter the first number:12.5 Enter the next number:3 Enter an operator(+,-,*,/): * The final result is:37.5 Author’s Comment: Chapter 8 can help you evaluate all the valid inputs. In this project, you add one simple validation. I want that you consider a case when a user supplies an invalid operator. In this case, your application should tell the user about this error. Here is a sample output for this negative scenario: ========================= This is a simple calculator. It supports the following operations: i)Addition ii)Subtraction iii)Multiplication and iv)Division. ========================= Enter the first number:2 Enter the next number:5 Enter an operator(+,-,*,/): > Invalid operator. Cannot compute the result. CS 5.2 Problem Statement You can organize the CS 4.1 implementation in a better way. Your guess is correct. I want that you use functions in that code. Chapter 7: Functions and Modules This chapter covers user-defined functions, lambda functions, and modules. These are very common in Python programming. You use them to organize your code. Function Overview We can think of a function as a logical set of statements that perform a specific task. You use functions to divide our code into manageable pieces. They allow you to optimize and reuse your code. Consider a simple scenario. Suppose you have written a function that can calculate a sum of two numbers. So, whenever you want to compute the sum of two numbers, instead of rewriting the code, you can simply invoke this function. When you see a function usage in someone’s code, you notice the following things: They define the function to describe behaviors. They call the function single or multiple times. An ordinary function has a name, a body. It can have zero, one, or more parameters. You will be familiar with all of them shortly. Before I discuss more on functions, let me cover the following points: A function definition starts with the executable code called, def. Then you type the function name with parameters (if any). Finally, a def statement ends with a colon. I told you that a function may not have parameters. So, let me show you a simple function with no parameter. This function can print hello. def print_hello(): print("Hello") You can invoke this function using the following code: print_hello(). You’ll see the complete demonstration shortly. The statements of the body of the function are indented. This format is used to show that all these statements belong to the function. POINTS TO REMEMBER If you use PyCharm IDE, once you press enter to go to the next line, this indentation is automatic. Otherwise, you can simply use the same characters for indentation. For example, you can use a tab or space; but you should not mix both. Your function can have parameters. For example, consider the following function which takes two parameters-the first one is for a name , the second one is for age . Here is the function, called print_details for you: def print_details(name,age): print(f"Hello {name}! How are you?") print(f"You are now {age}.") As a result, I can use the following line of code: print_details("Bob",20) When I execute this code, I expect to see the following output: Hello Bob! How are you? You are now 20. You can have function documentation. Function documentation helps others to understand what the function does. You can also print this documentation using <your_function_name>.__doc__ . Note that you’re seeing the use of double underscores here. To understand this, let me add some documentation to the previous function which is as follows: def print_details(name,age): """ This function takes two parameters. You can supply the name and age of the user in this function. """ print(f"Hello {name}!How are you?") print(f"You are now {age}.") Now you can print this documentation using the following line of code: print(print_details.__doc__) Demonstration 1 Go through the following demonstration. It contains all the functions and associated codes that I have discussed so far. #Function example-1 print("***Function example-1.***") def print_hello(): print("Hello") print_hello() #Function example-2 print("\n***Function example-2.***") def print_details(name, age): """ This function takes two parameters. You can supply the name and age of the user in this function. """ print(f"Hello {name}! How are you?") print(f"You are now {age}.") print_details("Bob", 20) Output Here is the output. ***Function example-1.*** Hello ***Function example-2.*** Hello Bob! How are you? You are now 20. Analysis Notice that when I call print_details("Bob", 20) , I pass two arguments in the function call. The first one is a string and the second is a number. This shows the fact that a function can have multiple parameters that can be of different types. Before you see some other characteristics of functions, let’s have a quick discussion about argument and parameter. Argument vs Parameter People often use the words arguments and parameters interchangeably. But expert programmers are particular about this. The variables in a function definition are called parameters of the function. For example, when you see the following function definition: def print_details(name,age): //some code The name and age are called parameters(or, formal parameters) of the function print_details . But, when you invoke the function using the following code: print_details("Bob",20), we say that “Bob” and 20 are the arguments you’ve passed to this function. Similarly, in the line print_details("Sam",30) , “Sam” and 30 are arguments. Many developers refer to them as actual parameters as well. You can say that we pass the arguments to a function. These values are assigned to the function parameters. But I already mentioned that some programmers do not emphasize these terms too much. So, they interchangeably use these terms. You know about arguments and parameters now. Let us continue the discussion on some other characteristics of functions. You can repeat function calls and you can vary the arguments. In demonstration1, you have seen the following line: print_details("Bob",20) You can repeat the line as many times as you want. You can also vary the arguments of your function. So, the following segment of code: def print_details(name,age): """ This function takes two parameters. You can supply the name and age of the user in this function. """ print(f"Hello {name}! How are you?") print(f"You are now {age}.") print_details("Bob", 20) #You can repeat function calls. print_details("Bob", 20) # You can vary the arguments of the functions print_details("Sam", 35) Can produce the following output. Hello Bob! How are you? You are now 20. Hello Bob! How are you? You are now 20. Hello Sam! How are you? You are now 35. Discussion on Function Arguments Now you know about function arguments and parameters. These are essential to understand the upcoming section. Let us take a deeper look at a function argument now. Positional Argument In the previous segment of code, instead of writing the following code: print_details("Bob",20) if you write: print_details(20,"Bob") # Unexpected outcome You’ll receive the following output: Hello 20! How are you? You are now Bob Oh..no! How is this possible? Yeah, you’ve guessed it right. You needed to pass the arguments in the correct order. For example, in this case, 20 is assigned for the name parameter( as it was in the first position in function definition) and ‘Bob’ is assigned for the age parameter(which is in the second position in function definition). Unless you specify these differently, by default, Python expects that you pass the arguments following the order of the parameters which are defined in the function definition. When the values are matched in this way, you say that you are following positional arguments. Keyword Arguments Let us invoke the function differently. This time, I use the following lines: print_details(age=20,name="Bob") # Expected outcome print_details(name="Bob", age=20) # Again expected outcome As mentioned in the comments, in both cases, you get the expected result. You can verify the result when you execute the code. For example, in the previous demonstration, execute the following two lines of code. You’ll see the following output: Hello Bob! How are you? You are now 20 Hello Bob! How are you? You are now 20 So, what we see now is that when you pass arguments in this namevalue pair, you will always see the expected result. Programmatically, these are called keyword arguments. Use of Default Values You always want to make your programming life easier. So, instead of supplying values in each case, you may be interested to use some default values. There are several reasons for this, but I am listing a few of them: You discover that in a specific function call, a particular argument always gets the same value. You want to type less. You do not wish to pass all values in your function invocation. You are not sure about the accepted value of a parameter. And so forth. To address this type of concern, let’s modify the previous function definition as follows: #Using default values in a function def print_details(name="Sam",age=35): """ This function takes two parameters. You can supply the name and age of the user in this function. By default, the name is 'Sam' and the age is 35. """ print(f"Hello {name}! How are you?") print(f"You are now {age}.") Notice the presence of (name="Sam”,age="35") in the updated function. I’ve updated the function documentation too. You can see that I’ve added the following line: By default, the name is 'Sam' and the age is 35. You know that it is a documentation comment and it is optional for you. But it can provide better readability. The changes in the modified function definition tell us- now onwards, you can invoke the function with or without parameters. It is because you have supplied default values for all these parameters. Let us test this. Use the following lines of codes now and refer to the associated comments for a better understanding. print_details()#Will take both the default values print_details(name="Jack")#Will take age=35 as default print_details(age=45)#Will take name="Sam" as default #None of the default values are considered now print_details("Bob",20) When you execute this segment of code, you’re expected to see the following output: Hello Sam! How are you? You are now 35. Hello Jack! How are you? You are now 35. Hello Sam! How are you? You are now 45. Hello Bob! How are you? You are now 20. Demonstration 2 Here is the complete demonstration for you. #Using default values in a function def print_details(name="Sam",age=35): """ This function takes two parameters. You can supply the name and age of the user in this function. By default, the name is 'Sam' and the age is 35. """ print(f"Hello {name}! How are you?") print(f"You are now {age}.") print_details() #Will take both the default values print_details(name="Jack") #Will take age=35 as default print_details(age=45) #Will take name="Sam" as default #None of the default values are considered print_details("Bob", 20) Output Here is the output. I’ve marked the default values in the output in bold. Hello Sam! How are you? You are now 35 Hello Jack! How are you? You are now 35 Hello Sam! How are you? You are now 45 Hello Bob! How are you? You are now 20 Demonstration 3 In this demonstration, I calculate the sum of the two numbers. Here is my function: def calculate_sum(x, y): return x + y Using this function, I can calculate the sum of 12 and 15 . Using the same function, I can also the sum of 20.5 and 37 . So, you can see that I do not need to write the programming logic repeatedly. This demonstrates the concept of code reuse using functions. Let us go through it. def calculate_sum(x, y): return x + y total = calculate_sum(12, 15) print(f"The sum of 12 and 15 is:{total}") total = calculate_sum(20.5, 37) print(f"The sum of 20.5 and 37 is:{total}") Output Here is the output. The sum of 12 and 15 is:27 The sum of 20.5 and 37 is:57.5 Analysis If you write the following: total = calculate_sum('10','20') #1020 print(f"The sum of '10'' and '20' is:{total}") you see the following output: The sum of '10'' and '20' is:1020 You can guess the reason. This time you used two strings, instead of two numbers. This is why you see a concatenated string ( 1020 ) in the output. Discussion on Return Values In demonstration1, you used the function print_hello() as follows: def print_hello(): print("Hello") and when you used the following line: print_hello(), you could see the output immediately. Again, in demonstration3, you saw the following lines: def calculate_sum(x, y): return x + y total = calculate_sum(12, 15) print(f"The sum of 12 and 15 is:{total}") but this time the calculate_sum() function took two numbers as arguments; adds them and stores the result into another variable called total . So, you could see the result when you display the value inside the total variable. This example shows you a simple fact: When you use a function, you do not need to print the result immediately. Instead, you can store the value that comes out from a function into another variable. This value is called the return value of a function. You often see that a function returns only one value. But using a smart program, you can return multiple values from a function too. I have already shown you a function with no return values and a function with only one return value. This time let us concentrate on a function that can return multiple values. Demonstration 4 In this example, I have a simple list called initial_list . This list contains the values 1,2,3,4, and 5. I also have a function called make_double(). This function takes a list as an argument, makes the elements double. Finally, it appends the result into another list called double_list which was initially empty. When you invoke the make_double() function and pass the initial_list as an argument, I show you a way how to return multiple values from a function. I print the double_list at the end of this program. You can see all the double values of the original integers which I initially supplied through a list. def make_double(input_list): """ It is a function that can return multiple values. Each element in the list will be doubled by this function. """ for element in input_list: double_list.append(2 * element) print("The initial_list is :") print(initial_list) print("Calling the function make_double() now.") make_double(initial_list) print("The double_list is :") print(double_list) print("The initial_list at present :") print(initial_list) #unchanged initial list Output Here is the output. The initial_list is : [1, 2, 3, 4, 5] Calling the function make_double() now. The double_list is : [2, 4, 6, 8, 10] The initial_list at present : [1, 2, 3, 4, 5] Demonstration 5 In the previous demonstration, you did not modify the original list. But in some cases, once you create a new list, you do not need to maintain the old/original list. But keep in mind that this can be risky. The following demonstration shows you such an example, where you remove the elements from the initial list, make them double, and create a new list. So, in the end, when you print the original list, you’ll see that the initial list is empty. initial_list = [10, 20, 30, 40, 50] double_list = [] def make_double(input_list): """ It is a function that can return multiple values. Each element in the list will be doubled by this function. """ while input_list: element = initial_list.pop() #print(element) double_list.append(2 * element) print("The initial_list is:") print(initial_list) print("Calling the function make_double() now.") make_double(initial_list) print("The double_list is:") print(double_list) print("The initial_list at present :") print(initial_list) Output Here is the output. The initial_list is: [10, 20, 30, 40, 50] Calling the function make_double() now. The double_list is: [100, 80, 60, 40, 20] The initial_list at present : [] Lambda Function Lambda functions fall into advanced concepts, but I want you to know about this. A lambda function is a function with no name. This anonymous function can take one or multiple arguments, but a single expression. A common function starts with the def keyword. A lambda function starts with a lambda keyword. Demonstration 6 This demonstration uses two lambda functions. The first one does not take any argument. It simply prints Hello, Reader! The next one takes one argument and doubles a number. print("***Lambda function example-1.***") say_hello = lambda: print("Hello, Reader!") say_hello() print("***Lambda function example-2.***") make_double = lambda x:x * 2 print(f"Double of 10 is:{make_double(10)}") print(f"Double of 25.35 is:{make_double(25.35)}") Output Here is the output. ***Lambda function example-1.*** Hello, Reader! ***Lambda function example-2.*** Double of 10 is:20 Double of 25.35 is:50.7 Demonstration 7 You can use lambda functions as an argument for another function. For example, in the following in-built map() function, I use a lambda function and a list as arguments. The initial list contains some numbers. I use the lambda function to increment each number by 2 in the list. The map function requires a function object and any number of iterables, such as a list or a dictionary. So, in the following demonstration, I pass a lambda function and a list to fulfill the need. Go through the following demonstration and output for your reference. original_list = [1, 2, 3, 4, 5] # Adds 2 to each item in the list new_list = list(map(lambda x: x + 2, original_list)) print("The original list is as follows:") print(original_list) print("The updated list is as follows:") print(new_list) Output Here is the output. The original list is as follows: [1, 2, 3, 4, 5] An updated list is as follows: [3, 4, 5, 6, 7] POINT TO NOTE In this example, you see a warning message which says: PEP 8: E731 do not assign a lambda expression, use a def. It means that Python recommends you to write code like: def say_hello(): print("Hello, Reader!") say_hello() In the end, it is your choice which approach you want to follow. Modules In real-world applications, normally the code size is gigantic. If you place the entire code is in a single place, you create a big file. This kind of practice is commonly discouraged. Instead, Python allows you to place your code in a separate file with a .py extension. Using an import statement, you make these codes available in a current file. What are the advantages? Go through the following bullet points: You make the current program file small and focus on the high-level logic in this file. The inner working logic is hidden. Because that part of the code is written in a separate file that is not immediately visible to the user. A module can contain many things. For example, you can place variables, functions, and classes in a module. In Chapter 11, I have shown you how to import a single class, multiple classes, or the entire module. I have also discussed what are the recommended practices. To avoid repetition, now I show you some simple usage of a module that contains the variables and functions only. Let us make a file called my_library.py and place the following codes into it. You can see that this file contains a list, a dictionary, and two functions. On my computer, I’ve organized the codes chapter-wise. For example, I stored all programs of the chapter7 inside a directory named chapter7 . The parent directory of the chapter7 is PythonCrashCourse . If I store the my_library.py inside the chapter7 directory, every time I refer to this module, I need to mention it as chapter7.my_library . To avoid less typing, I store this inside PythonCrashCourse . Now I can directly refer to the module without mentioning the chapter7 . # A simple list my_list = [1, 2, 3, 4, 5] # A simple dictionary my_dictionary = {1: "Jack", 2: "Kate", 3: "Bob"} def make_total_double(first, second): """ This function takes two numbers and returns the double. """ total = first + second return total * 2 def make_average(first, second, third=0): """ This function takes three numbers and returns the average. The third number is optional. """ total = first + second + third return total / 3 Now create a file using_molules_example_1.py inside chapter7 and write the following lines of code: from my_library import my_list, my_dictionary print("Accessing some list elements:") print(f"my_list[0]={my_list[0]}") print(f"my_list[3]={my_list[3]}") print("\nAccessing some dictionary elements:") print(f"my_dictionary[1]={my_dictionary[1]}") print(f"my_dictionary[2]={my_dictionary[2]}") If you run this code, you see the following output: Accessing some list elements: my_list[0]=1 my_list[3]=4 Accessing some dictionary elements: my_dictionary[1]=Jack my_dictionary[2]=Kate You can see that I can access the list and dictionary elements from this file, though these elements live in a separate file. I can access them because I use the following statement at the beginning of the file: from my_library import my_list, my_dictionary In the same way, you can use the functions from my_library.py . To test this, make the following function available using an import statement. Now you write the following lines of code in this file. from my_library import make_total_double print("Using the make_total_double function now.") result = make_total_double(20, 30.5) print(result) This code segment can produce the following output: Using the make_total_double function now. 101.0 Hope you understand the key usage of the module. Let us gather all these points in the following demonstration. Demonstration 8 Here is the content of using_molules_example_1.py for you. from my_library import my_list, my_dictionary, make_total_double print("Accessing some list elements:") print(f"my_list[0]={my_list[0]}") print(f"my_list[3]={my_list[3]}") print("\nAccessing some dictionary elements:") print(f"my_dictionary[1]={my_dictionary[1]}") print(f"my_dictionary[2]={my_dictionary[2]}") print("\nUsing the make_total_double function now.") result = make_total_double(20, 30.5) print(result) Output Accessing some list elements: my_list[0]=1 my_list[3]=4 Accessing some dictionary elements: my_dictionary[1]=Jack my_dictionary[2]=Kate Using the make_total_double function now. 101.0 You can see that when Python reads the following line: from my_library import my_list, my_dictionary, make_total_double It makes my_list, my_dictionary, and make_total_double(…) available in the current program file. Already this line of code is long. The my_library.py may contain additional functions and variables. So, you may want to import the entire content from the file into your current program. The following example illustrates this case where I make the key changes in bold. import my_library # imports the whole module. print("Accessing some list elements:") print(f"my_list[0]={my_library.my_list[0]}") print(f"my_list[3]={my_library.my_list[3]}") print("\nAccessing some dictionary elements:") print(f"my_dictionary[1]={my_library.my_dictionary[1]}") print(f"my_dictionary[2]={my_library.my_dictionary[2]}") print("\nUsing the make_total_double function now.") result = my_library.make_total_double(20, 30.5) print(result) When you run this program, you see the same output which you saw in the previous demonstration. POINT TO REMEMBER You can import the whole module, or you can import a particular function from a module. I have shown you both approaches. If you import the whole module, you need to use the following syntax: module_name.function_name() to access a function from the module. The same comment applies to variables and classes of the module. In Chapter 11, I discuss more on modules. Using Alias Consider a typical case when you see a big module name and you need to refer to it many times from different files. Here typing this long name repeatedly becomes a boring activity for you. Consider another case. Suppose you import a module in the current program. But a function in the current program and a function in the module both may have the same name. How can you proceed now? In such a situation, you can use an alias. You can consider the alias as a short name for your function or module. Let us test a function alias. In the following demonstration, I make an alias for the function make_total_double function. I have chosen the alias name as mtd . This is why you see the following line of code: from my_library import make_total_double as mtd The remaining program is easy to understand. Go through it. Demonstration 9 For the following demonstration, I’ve created a new file, called using_function_alias.py . Here is the content. from my_library import make_total_double as mtd print("\nUsing the make_total_double function now.") print("Here mtd is an alias for make_total_double(...)") result = mtd(20,30.5) print(result) Output When you run the program, you see the following output: Using the make_total_double function now. Here mtd is an alias for make_total_double(...) 101.0 Let us test a module alias. In the following demonstration, I make an alias for the module my_library . I have chosen the alias name as ml . So, you see the following line of code in the upcoming demonstration: import my_library as ml Demonstration 10 For the following demonstration, I’ve created another new file, called using_module_alias.py . Here is the content. import my_library as ml # imports the whole module. print("Accessing a list element:") print(f"my_list[0]={ml.my_list[0]}") print("\nAccessing a dictionary element:") print(f"my_dictionary[1]={ml.my_dictionary[1]}") print("\nUsing the make_total_double function now.") result = ml.make_total_double(20, 30.5) print(result) Output When you run the program, you see the following output: Accessing a list element: my_list[0]=1 Accessing a dictionary element: my_dictionary[1]=Jack Using the make_total_double function now. 101.0 Quick Discussion on pip The pip is the standard package manager for Python. You often use it to install the additional packages (or modules) that you do not find in the Python standard library. I give you an example. In Chapter 3, I imported the math module to get functions sqrt(), ceil(),floor() etc. So, I used the following line: from math import *; Now the question may come to your mind: where does this math module reside? You can find it in the Python module index which is at https://docs.python.org/3/py-modindex.html. But you cannot see the NumPy library here. If you work on machine learning and you use Python, you often need this library. In that case, you need to install this library using the following command: pip install numpy So, you can see that the install command helps you to install a module using pip . If you are using Python3 >=3.4, you already have pip on your computer. For example, in my Windows system, I can use the following command to check whether pip is installed on my computer: C:\Users\Vaskaran Sarcar>py -m pip --version pip 19.2.3 from C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\site-packages\pip (python 3.8) For other operating system commands and to know more about pip you can refer to the online link https://pip.pypa.io/en/stable/installing/ This book targets the domain of beginners and I do not need the outside packages to describe these materials. So, I exclude the detailed discussion of pip in this book. EXERCISE E7.1 Can you predict the output of the following code segment? print("Exercise-7.1") def print_x(x): print(x) def print_y(y): print(y) def main(): print_x(10) print_y(20) main() E7.2 Can you predict the output of the following code segment? def print_me(x): x += 2 print(x) def print_me(x): x += 3 print(x) print_me(5) E7.3 Can you predict the output of the following code segment? def print_me(x): x += 2 print(x) def print_me(y): y += 3 print(y) print_me(5) E7.4 Can you predict the output of the following code segment? x = 10 print(f"x={x}") def print_me(x): x += 2 print(f"Now x={x}") print_me(x) print(f"Here x={x}") E7.5 Can you predict the output of the following code segment? def print_me(x): print(x) def print_me(x,y): print(x) print(y) print_me(5) print_me(5,7) E7.6 Suppose you have a list of numbers. Using a lambda function, can you increase each number by 5%? Solution to Exercises E7.1 10 20 Explanation: This is straightforward. The main() function calls the other two functions. So, when you call main(), it calls print_x() and print_y() too. E7.2 8 Explanation: Method overloading allows you to use the same name for multiple functions which can be worked differently. We often see it in C#, Java, C++, etc. But Python does not this concept by default. If you want the concept, you need to write your code differently. Python does not show you any error if you use the same name for two functions, but it calls the latest one. In this exercise, both functions exist with the same name print_x(). But the last same-named function increments the value of x by 3 . This is why print_x(5) results in 5+3=8 E7.3 8 Explanation: Previous explanations can explain this one, too. I have put this exercise for you to note the following fact: If you only change the method parameter from x to y; it does not mean that you have a different function. E7.4 x=10 Now x=12 Here x=10 Explanation: The initial value of x is 10 . Then I called print_me() which increments the value to 12 . But notice that this change has happened inside the function body. It means that I worked on a local copy of x . So, when I come out from the function body, I again have the value 10 for x . We lose the value of a local variable between function invocations. You can use the same local variable for different functions too. In simple terms, the memory of a local variable is used when it is in the scope. When you leave the scope, you free the memory. Sometimes, you may want to a have variable that does not die before your program ends. In such a case, you can use a global variable. Since global variables are not for any specific functions, you place them outside of all functions. In Python, global is a reserved word. You can use it as follows: print("Example of a global variable.") x = 10 print(f"x={x}") def print_me(): global x x += 2 print(f"Now x={x}") print_me() print(f"Here x={x}") print("-" * 30) Now you can get the following output: Example of a global variable. x=10 Now x=12 Here x=12 E7.5 Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter7/chap7_exercise.py", line 79, in <module> print_me(5) # error now TypeError: print_me() missing 1 required positional argument: 'y' Explanation: I have placed all exercises in the same file, so you see line number 79. This line number can vary if you make separate files for each exercise. The important part is to recognize the error in the pointed area. Here you see the error because I invoke print_me() with one argument. In exercise E7.2 , you have seen the explanation. If you have multiple functions with the same name, Python considers the latest one. So, in this case, Python wants us to supply two arguments because the latest definition of print_me() has two parameters. E7.6 original_list = [100, 200, 300, 400, 500] # Adds 2 to each item in the list new_list = list(map(lambda x: x * 1.05, original_list)) print("The original list is as follows:") print(original_list) print("The updated list is as follows:") print(new_list) Output The original list is as follows: [100, 200, 300, 400, 500] The updated list is as follows: [105.0, 210.0, 315.0, 420.0, 525.0] CS 5.1 Implementation I use many small functions in this implementation. I convert a valid operand into a float before I calculate the result. So, you’ll find codes like: usr_input1 = input("Enter first number:") first_number = float(usr_input1) I store valid operators in a list. Here is the code: valid_operators = ["+", "-", "*", "/"] If the operator is not included in this list, I report an error. I use an if-else chain to do this task: if usr_opr in valid_operators: compute(first_number, usr_opr, second_number) else: print("Invalid operator. Cannot compute the result.") The remaining codes are easy to understand. Refer to the supporting comments if you need them. Here is the complete implementation: print("=" * 25) print("This is a simple calculator.") print("It supports the following operations:") print("i)Addition" "\nii)Subtraction" "\niii)Multiplication and " "\niv)Division.") print("=" * 25) valid_operators = ["+", "-", "*", "/"] def add_numbers(num1, num2): """ Adds the numbers. """ return num1 + num2 def subtract_numbers(num1, num2): """ Subtracts the numbers. """ return num1 - num2 def multiply_numbers(num1, num2): """ Multiplies the numbers. """ return num1 * num2 def divide_numbers(num1, num2): """ Divide num1 by num2. """ return num1 / num2 def compute(num1, operator, num2): """ This function computes the final result. """ result = 0 #default value if operator == '+': result = add_numbers(num1,num2) elif operator == '-': result = subtract_numbers(num1,num2) elif operator == '*': result = multiply_numbers(num1,num2) else: result = divide_numbers(num1, num2) print(f"The final result is:{result}") def main(): """ This is the top-level function. It calls the compute() function. """ usr_input1 = input("Enter the first number:") first_number = float(usr_input1) usr_input2 = input("Enter the next number:") second_number = float(usr_input2) usr_opr = input("Enter an operator(+,-,*,/): ") if usr_opr in valid_operators: compute(first_number, usr_opr, second_number) else: print("Invalid operator. Cannot compute the result.") main() Possible Improvements Try to validate all the user inputs when you learn the exception handling mechanism. You’ll see a sample implementation at the end of Chapter 8. You can support more operations in your calculator. CS 5.2 Implementation Here is an improved and organized solution for CS 4.1 using functions. # All questions question1 = "Q1.What is the value of the expression:2*3-4?" \ "\n(a)1" \ "\n(b)2" \ "\n(c)3" \ "\n(d)None." question2 = "\nQ2.What is the value of the expression:1+(2*3)/4?" \ "\n(a)1.5" \ "\n(b)3" \ "\n(c)3.5" \ "\n(d)None." question3 = "\nQ3.The set data type can hold duplicate values." \ "The statement is:" \ "\n(a)False" \ "\n(b)True" \ "\n(c)Partially correct." \ "\n(d)None." # Storing the questions with answer keys # inside the following dictionary. question_bank = {question1: "b", question2: "c", question3: "a"} def run_test(questions): """ This function takes the question bank as a parameter. You can supply the question bank with answer keys in this function. """ print("Welcome to the MCQ test.") print("=" * 25) score = 0 # initial value for key in questions: print(key) user_input = input("Type your answer(a/b/c/d):") if user_input == question_bank[key]: score += 1 print(f"\nYour Score:{score} out of {len(questions)}") run_test(question_bank) Possible Improvements Validate all the user inputs when you learn the exception handling mechanism in Chapter 8. Case Study VI CS 6.1 Problem Statement In CS 5.1, you made a calculator that performs the basic operations-addition, subtraction, multiplication, and division. In Chapter8, you learn about exception handling. Upon completion, you should be able to evaluate the user inputs and valid operations. Now I want you to make a better implementation using that knowledge. The output should not change for valid inputs. But for a negative input, the application should raise exceptions. For example, the application should detect whether you try to divide a number by zero. If so, it raises the exception. I give you three sample outputs to understand it better. Here is the sample-1. A user enters an invalid number. Since the number is invalid, you do not need to ask for an operator. So, you can report the issue immediately. ========================= This is a simple calculator. It supports the following operations: i)Addition ii)Subtraction iii)Multiplication and iv)Division. ========================= Enter the first number:23 Enter the next number:asc Invalid input.Details: could not convert string to float: 'asc' Here is the sample-2. A user enters an invalid operator. ========================= This is a simple calculator. It supports the following operations: i)Addition ii)Subtraction iii)Multiplication and iv)Division. ========================= Enter the first number:12.5 Enter the next number:3 Enter an operator(+,-,*,/): >? Error details: Invalid operator. Here is the sample-3. A user tries to do an invalid operation. ========================= This is a simple calculator. It supports the following operations: i)Addition ii)Subtraction iii)Multiplication and iv)Division. ========================= Enter the first number:27.3 Enter the next number:0 Enter an operator(+,-,*,/): / Invalid Operation.Details: float division by zero Chapter 8: Exception Management Coding is fun. You may face continuous challenges and encounter sudden surprises during your program executions. Many of the failures are beyond the control of a programmer. These surprises may occur for various reasons. I list some of them as follows: There are some careless mistakes (such as typos) in the program. The program logic is incorrect. A developer ignores/overlooks the possible loopholes in the code, etc. Programmers often term these unusual situations as exceptions. Handling these exceptions is essential when you write an application. The core concept is not new. It has been around for some time. Python has its own set of exceptions. These are available for your immediate use. You can also write a custom exception to handle a specific situation in your way. This chapter covers a detailed discussion about the exception handling mechanism. Types of Mistakes You can broadly classify the mistakes (or errors) in a program in two categories: Before you execute a program, often termed as compiletime errors. (It is important to note that there are some differences between a compiler and interpreter. We often term Python as an interpreted language. It is partially true because there is a step for compilation too. You will see the discussion shortly). When the program is running, often termed as run-time errors. When I say “Before you execute a program”, I mean that some errors are detected by Python in the early development stage. Python acts as your friend to figure out the error details. These errors are simple to detect and fix. For example, it may point out some line number (where the error is encountered) and display a brief description of the error. Once you correct it, you need to recheck whether the updated program is ready for execution or not. Sometimes you need to fix multiple errors. In those cases, we need multiple rechecks. I told you earlier that “Python is an interpreted language”. This statement is partially true. To elaborate, let me write the following two lines of code in a notepad and save it as hello_python.py : print("Hello Python!") print("This is Vaskaran.") I put this file in my directory C:\TestClass . So, currently, on my computer I can see this (Refer Figure 8-1): Figure 8-1:The hello_python.py is placed inside C:\TestClass. Using the command prompt, I enter the directory which contains the python file. Then I execute the following command: python -m py_compile hello.py Here is the screenshot (See Figure 8-2): Figure 8-2: The hello_python.py is compiled successfully. Now, you’ll notice that a folder called __pycache__ is created. (Note that here you see a double underscore) Figure 8-3: A new folder __pycache__ is created inside C:\TestPython If you enter this newly created folder, you will see a compiled Python file that has a name similar to hello_python.cpython-38 .You see ‘38’ because I used the Python version is 3.8. So, this number can vary in your case. Notice the following screen (See Figure 8-4): Figure 8-4: The current contents inside __pycache__ Now you can run this compiled file using the following command from the command prompt: python hello_python.cpython-38.pyc Here is a screenshot for you (See Figure 8-5): Figure 8-5: Successful execution of the file inside __pycache__ directory Note that if there is any syntax error in your file, Python does not generate a compiled file for you. To test this, consider a new file, called hello_python2.py . This file includes some typographical errors. Here is the content of it: print("Hello Python!" #error- ) missing If you try the previous way to generate a compiled file from it, you get some errors. I show the important error segment for your immediate reference. C:\TestPython>python -m py_compile hello_python2.py Traceback (most recent call last): File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\py_compile.py", line 144, in compile code = loader.source_to_code(source_bytes, dfile or file, File "<frozen importlib._bootstrap_external>", line 846, in source_to_code File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed File "hello_python2.py", line 2 ^ SyntaxError: unexpected EOF while parsing During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\py_compile.py", line 209, in main compile(filename, doraise=True) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\py_compile.py", line 150, in compile raise py_exc __main__.PyCompileError: File "hello_python2.py", line 2 ^ SyntaxError: unexpected EOF while parsing … If you use PyCharm IDE, it can point the error easily with the red markers (See Figure 8-6): Figure 8-6: The PyCharm IDE shows the typographical error. Move your cursor to the highlighted area. You understand that you forgot to put the closing bracket for the print() function. This type of error is common, but sometimes hard to find with human eyes at the very beginning. So, a Python compiler or an IDE like PyCharm can help you in these situations. On the other hand, run-time errors are challenging. Here, you get a green signal from the compiler and everything appears to be fine. But your program still produces wrong results and may terminate prematurely. Here are some examples of run-time errors that can be caused for the following operations: ● Dividing an integer by 0 ● Try to convert an invalid string to an Integer ● An expected file is NOT found, or it doesn’t exist at all, and so forth. General Philosophy The primary purpose of exception handling is to handle errors that arise during program execution. You can use it effectively for many other purposes, too. Programmers are often lazy to analyze all the possible errors in advance. It can make sense for a small application where debugging the code is easy. But consider the case when you have a big application with many small functions. If you need to consider all possible errors in every code segment, the task can be repetitive. Consider a simple example. Suppose your application computes something and you periodically store the result in a disk. These operations can be interrupted in various ways. Here are some possibilities: A user provides one or more invalid inputs. At some stage, you encounter something unwanted, say a division by zero operations. The disk does not have enough space. The disk is not fully functional. So, you cannot save the result. and so forth. For the ultimate safety, let’s assume that you check all possible error conditions in advance. Under this method, the error detecting and handling code become a significant portion of the entire program. This code is repetitive and scattered. It is not desirable. Exception handling mechanisms can act as a bridge between these possibilities. You understand that at a high level, you can consider two imaginable types of errors-one to detect a computational error, and another one to detect disk errors. So, you make two handlers to handle these situations. Then you organize your code in such a way that if any of these errors occur, you refer to the corresponding error handler. POINT TO REMEMBER Usually, the problems that can cause an exception are different. For example, an arithmetic problem such as division by 0 differs completely from a memory shortage problem in a disk. Therefore, use a particular handler to manage a specific type of problem. Common terms You can define an exception as an event that breaks the normal execution flow of the program during the runtime of a program. Python creates some special objects called exceptions to manage the errors in those situations. We say that a particular block of code raises (or throws) an exception. We call the act of responding to an exception catching an exception. We refer to the code that handles the exception as an exception handler. You can see multiple exception handlers in a program. They help you catch different types of exceptions in the code. It is also possible that different handlers handle the same exception, but in general, they do not live in the same place. If you have the correct exception handlers, your program continues to run. Otherwise, the program may halt and die prematurely. In these cases, follow the traceback, which includes a detailed report about the situation. POINTS TO REMEMBER An exception handling mechanism deals with runtime errors, and if not handled properly, an application will produce unwanted output and it may die prematurely. Therefore, try to write applications that can detect and handle surprises gracefully and prevent the premature death of the application. Python Exceptions Python generates an exception object with a raise statement. Once it is raised, Python follows the common exception handling mechanism, i.e. instead of proceeding with the next statement, the current calling chain searches for the handler that can handle the situation. If it finds such a handler, it can access the exception object for more information. Otherwise, the program aborts with an error message. Python’s exception handling philosophy is a little different from other common programming languages such as Java, for example. In Java, developers check all possible errors at the beginning. It is because handling a run-time exception is a costly operation. We call this style as look before you leap (LBYL). Python likes to deal with exceptions after they occur. Yes, it is risky. But they encourage you to write code, which is less cumbersome and easily readable. We call this approach “Easier to Ask for Forgiveness than Permission (EAFP). It is often easier to ask for forgiveness than to ask for permission. — Grace Hopper, American computer scientist Python has its own set of exceptions. It has a hierarchical structure. At the time of this writing, Python 3.9 is just released. So, you can get the latest list from the link: http://bit.ly/python-exceptions. Instead of filling the pages with the complete list of exceptions, I prefer to show a subset from it. You can get the idea from the following list: BaseException SystemExit … Exception ArithmeticError FloatingPointError ZeroDivisionError AssertionError … ValueError UnicodeError … … Notice the indentation. It is deliberate. Each exception type is a class. These classes follow the inheritance hierarchy. The hierarchical structure helps us to determine the parent-child relationship. Class, objects, and inheritance make the heart of object-oriented programming (OOP). You may be unfamiliar with OOP. Do not worry, it’s ok. I have included a discussion about them in the last part of the book. Following the list, it’s enough for you to know that a ZeroDivisionError is one type ArithmeticError which is again one type of Exception . But SystemExit is not an Exception type, but it is a BaseException type. You can get the full meaning of each type from the Python documentation. You can be familiar with them as you continue writing programs. POINT TO REMEMBER Python’s exception handling mechanism follows the object-oriented programming (OOP) style. Chapter 10 and Chapter 11 in this book discuss the OOP. You do not need to learn the OOP in detail to understand the material in this chapter. Demonstration 1 Let us examine the case when you do not handle the situation. You leave the decision to Python to manage the error. The following program seems to be fine and compiles successfully. But it can raise the exception during runtime because of invalid input (s). Let us test this program with some valid and invalid inputs. This program expects you to supply two valid integers. Then it displays the result of the division. Here is the program. # exception_handling_case_study_1.py a = input("Enter a valid integer:") b = input("Enter another valid integer:") c = int(a)/int(b) print(f"The result of a/b is:{c}") print("The program completes successfully.") Case Study with Valid Inputs You supply two valid inputs 7 and 2 in this case. And everything seems to work fine. Here is the output. Enter a valid integer:7 Enter another valid integer:2 The result of a/b is:3.5 The program completes successfully. Case Study with an Invalid Input This time you supply 7 and 0 . In this case, the application raises a run-time error. It is because you try to divide 7 by 0. Here are the inputs and corresponding output. Enter a valid integer:7 Enter another valid integer:0 Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter8/exception_handling_case_study_1. line 3, in <module> c = int(a)/int(b) ZeroDivisionError: division by zero Notice that Python generates a traceback for you. Following this report, you can identify that at line 3, you have encountered the problem. It is because there is an attempt of a division by zero. Once this error is raised, Python does not execute the next statement anymore. So, you do not see the line “ The program completes successfully.” in this output. Python names this kind of error as ZeroDivisionError . Case Study with another Invalid Input This time you supply abc and 2 . Since you cannot convert abc to a valid integer, the application produces a different run-time error. Here is the output. Enter a valid integer:abc Enter another valid integer:2 Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter8/exception_handling_case_study_1. line 3, in <module> c = int(a)/int(b) ValueError: invalid literal for int() with base 10: 'abc' Following this report, you can identify that this time you have encountered the problem because of an invalid literal for an integer. Python names this kind of error as ValueError . Once this error is raised, Python does not execute the next statement anymore. So, you do not see the line “ The program completes successfully.” in this output. Before you read further, I want you to note that an in-built Python function can raise the exception too. Here is such an example. I run this segment in a command shell. >>> mylist=[1,2,3,4] >>> mylist [1, 2, 3, 4] >>> mylist[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range You can raise an exception directly too. Here is an example: >>> raise ArithmeticError("Have Fun!") Traceback (most recent call last): File "<stdin>", line 1, in <module> ArithmeticError: Have Fun! Key Points of the Exception-handling Mechanism In this section, I highlight some key points about the exception handling mechanism. I suggest that you revisit these points as per your need. An exceptional object is created when a run-time error occurs. An exception is an object to describe an erroneous situation. An application can raise surprises during the application’s runtime. If such a situation occurs, in programming terminology, you say that the application has raised an exception, or it has thrown an exception. You can guard the situation using a try-except block. You instruct Python on what to do if an exceptional situation occurs. You place the code that may raise an exception inside a try block. You handle an exceptional situation inside an except block. If the codes inside the try block execute without an exception, the program control bypasses completely the except blocks. In the previous demonstration, you have seen that the same program can generate a variety of exceptions. So, you may notice multiple except blocks with a try block. When a particular except block handles the sudden surprise (the exception), you say that the except block has caught the exception. Let us assume a typical case. In a program, you have the code to handle the ZeroDivisionError , but you do not have except block to handle ValueError . In this case, if the ValueError occurs inside the try block, your program ends prematurely. When an exception occurs, Python shows you an error report. You can use this report to know the error location and the details of it. Demonstration 2 In demonstration2, you’ll examine all the key points that we have just discussed. Follow the code now. Again, you see case studies with valid and invalid inputs. This program is a modified version of demonstration1. So, it expects you to supply two valid integers before it displays the result of the division. # try_except_example_1.py print("The following program can handle the ZeroDivisionError and ValueError both.") a = input("Enter a valid integer:") b = input("Enter another valid integer:") try: result = int(a) / int(b) print(f"The result of the division is: {result}") except ZeroDivisionError as e: print("Invalid input! Your divisor becomes zero!") print(f"Error details:{e}") except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details: {e}") print("The program completes successfully.") Case Study with Valid Inputs You supply 2 valid inputs 7 and 2 in this case. And everything seems to work fine. Here is the output. The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:7 Enter another valid integer:2 The result of the division is: 3.5 The program completes successfully. Case Study with an Invalid Input This time you supply 7 and 0. Since the divisor is 0 before the division operation, the application could produce a run-time error, but you have handled it using an except block. Here is the output. The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:7 Enter another valid integer:0 Invalid input! Your divisor becomes zero! Error details: division by zero The program completes successfully. Notice that this time the program ends gracefully. You also see the line “ The program completes successfully.” in this output. It is because the error was caught successfully in an except block. This except block handles the ZeroDivisionError. In this block, you also print the custom messages to the user. When you do this, you provide better security to your application. It is because you do not disclose important details like your program file name, file location, etc. A skilled hacker can do illegal activities using this information. Case Study with another Invalid Input This time you supply 5 and abc . Since abc cannot be converted to a valid integer, the application could produce a run-time error. But in this program, I have another except block to handle this type of errors. We call it a ValueError . Here is the output. The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:5 Enter another valid integer:abc Invalid input! Provide correct input next time! Error details:invalid literal for int() with base 10: 'abc' The program completes successfully. Once again, since the program ended gracefully, you see the line “ The program completes successfully.” in this output. Notice that in this case, the error was caught in a different except block. Again, you have done an excellent job to prevent the skilled attacker by not disclosing the important details of the file. Demonstration 3 In this demonstration, I show you two important case studies. First, you’ll improve demonstration2. Then you’ll examine a case when a user provides different invalid inputs. Let us begin. I create a new file and name it use_of_try_except_and_else.py . Notice the program logic in the previous program and consider the division operation again. You understand that if a run-time error occurs during the division operation; the program does not print the result. This logic is correct, but you can beautify your code. It is a better idea to put the result of the division in an else block. It can show whether the code in the try block passes well. And as a next step, the program control enters the else block. So, in this example, I’ve moved the line: print(f"The result of the division is: {result}") into the else block. Here is the complete demonstration which can generate the same output. # use_of_try_except_and_else.py print("The following program can handle the ZeroDivisionError and ValueError both.") a = input("Enter a valid integer:") b = input("Enter another valid integer:") try: result = int(a) / int(b) #print(f"Result of the division is: {result}") except ZeroDivisionError as e: print("Invalid input! Your divisor becomes zero!") print(f"Error details:{e}") except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details:{e}") else: print(f"The result of the division is : {result}") print("The program completes successfully.") Case Study with Valid Inputs Let us test the program now. Supply two valid inputs: 15 and 3. Everything seems to work fine. Here is the output. The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:15 Enter another valid integer:3 The result of the division is: 5.0 The program completes successfully. Case Study with Invalid Inputs Let us pass abc and 0 this time to generate a run-time error. Notice that both inputs are invalid. Here is the output. The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:abc Enter another valid integer:0 Invalid input! Provide correct input next time! Error details:invalid literal for int() with base 10: 'abc' The program completes successfully. Analysis Here, both the inputs are invalid. The first one is abc, which you cannot convert to a valid integer. The second input is 0. It makes the divisor zero. But the program handled the first issue, which is a ValueError .Once you fix this error, you can see the next error. For example, supply 7 and 0 this time to get the following output: The following program can handle the ZeroDivisionError and ValueError both. Enter a valid integer:7 Enter another valid integer:0 Invalid input! Your divisor becomes zero! Error details: division by zero The program completes successfully. You can see that the program handles the error which appears first. In this context, the next section is very important. Read it carefully. POINTS TO REMEMBER When you assume that a particular segment of code may raise a runtime error or exception, you place that segment of code into a try block. To handle a specific run-time error, you place an appropriate except block to handle the error, print user-friendly messages and suppress important details to prevent malicious attacks. Finally, if there are codes that depend on the successful completion of the try block, you place them into the else block. In short, keeping this information in mind, you can design a try-except-else block. Use of pass Statement Sometimes you do not want users to see the exception details and allow the program to fail silently if a typical error occurs. You may want this to give the user a feel that everything is fine. For example, the upcoming program calculates the aggregate of two valid integers. If the user passes any invalid input, you catch the exception, but silently skip the exception details to the user using the pass statement. The following demonstration describes such a scenario. Demonstration 4 Here is the complete demonstration. # use_of_pass.py print("This program prints the sum of two valid integers.") first_input = input("Enter a valid integer:") second_input = input("Enter another valid integer:") total = 0 #default value try: a = int(first_input) b = int(second_input) except ValueError as e: pass else: total = a + b print(f"Sum of numbers:{total}") print("The program completes successfully.") Case Study with Valid Inputs You supply two valid inputs 27 and 5 in this case. And everything seems to work fine. Here is the output. This program prints the sum of two valid integers. Enter a valid integer:27 Enter another valid integer:5 Sum of numbers:32 The program completes successfully. Case Study with an Invalid Input Let us test the code with one set of invalid output too. Pass 27 and abc this time to generate a run-time error. Here is the output. This program prints the sum of two valid integers. Enter a valid integer:27 Enter another valid integer:abc The program completes successfully. Analysis This time, you do not see the sum; the program logic tells us that if everything goes well inside the try block, you calculate the aggregate of the valid integers inside the else block. But it is NOT the case here, because you have supplied a string abc which is not a valid integer. Arranging Multiple except Blocks In a program, you can expect different errors, and you guard them with appropriate except blocks. But you may not anticipate everything in advance. So, you may want to have a general except block that can handle remaining exceptional situations. For example, in demonstration2, you have handled both ZeroDivisionError and ValueError , but what happens if there is a different exception occurs in your program? In such a case, you can use a general except block to catch the remaining errors. Here is such an example when you see the following code: except Exception as e: print("An unknown error occurred.") I suggest that you add this block to your code. Ideally, it should be placed, after all the anticipated except block, something like the following: try: #Some code except ZeroDivisionError as e: print(f"Error details:{e}") except ValueError as e: print(f"Error details:{e}") except Exception as e: print(f"Error details:{e}") This arrangement is important. In the last block, you use the Exception class, which is a common base class for non-system-exiting exceptions. You may not know about “class” at this time. It is ok. You can understand the key thing: if you place a more generic or broader except block before a specific except block, your code will NOT reach the specific except block. Let us have a test in the following demonstration. First, I’ve placed the following block before other except blocks: except Exception as e: print("An unknown error occurred.") Here is the complete program where the except block arrangement is not proper. # try_with_multiple_except_example2.py print("***The following program can handle the non-system-exiting exceptions.***") a = input("Enter a valid integer:") b = input("Enter another valid integer:") try: result = int(a) / int(b) except Exception as e: print("An unknown error occurred.") print(f"Error details: {e}") except ZeroDivisionError as e: print("Invalid input! Your divisor becomes zero!") print(f"Error details: {e}") except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details: {e}") #except Exception as e: #print("An unknown error occurred.") else: print(f"Result of the division is : {result}") print("The program completes successfully.") Now run the program against the invalid inputs that you saw in demonstration2. Case Study with an Invalid Input First, you supply 7 and 0. Here is the output. ***The following program can handle the non-system-exiting exceptions.*** Enter a valid integer:7 Enter another valid integer:0 An unknown error occurred. Error details: division by zero The program completes successfully. Case Study with another Invalid Input This time you supply 5 and abc . Since abc cannot be converted to a valid integer, the application could produce a run-time error, and here is the output. ***The following program can handle the non-system-exiting exceptions.*** Enter a valid integer:5 Enter another valid integer:abc An unknown error occurred. Error details:invalid literal for int() with base 10: 'abc' The program completes successfully. In both cases, the program terminates gracefully, and you see the line “ An unknown error occurred . ” in the output. You can see that the toplevel except block was able to handle both kinds of errors. But you are unable to see the specific messages like: “ Invalid input! Your divisor becomes zero!”, or “ Invalid input! Provide a correct input next time!" in the output. If you want to see those specific messages, you need to place the except blocks properly. For example, here is a correct arrangement: #previous codes except ZeroDivisionError as e: print("Invalid input! Your divisor becomes zero!") print(f"Error details:{e}") except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details:{e}") except Exception as e: print("An unknown error occurred.") Now if you test your program against the same inputs, you’ll get the similar output that you saw in demonstration2. POINTS TO REMEMBER When you deal with multiple except blocks, you need to place more specific except blocks first. In other words, you should place the except blocks from most specific to most general. Custom Exception You can create a custom exception using the following code segment: class MyException(Exception): pass Let us use it now. The following program asks for user input. To make the example short and simple, I assume that the user needs to supply an integer that is less than or equal to 100. Otherwise, the program raises this custom exception, called MyException . Demonstration 5 Here is the complete demonstration. # custom_exception.py #Following example uses a custom exception class MyException(Exception): """ This is a custom exception.""" pass try: user_input = int(input("Enter an integer which is not greater than 100:")) if user_input > 100: raise MyException("You have made the integer greater than 100.") print(f"Well done.You have entered:{user_input}") except MyException as e: print(f"Custom exception is raised.Error Details:{e}") except ValueError as e: print(f"Here is the error Details:{e}") Output Here is a sample output. This time user supplies an integer less than 100. Enter an integer which is not greater than 100:23 Well done.You have entered:23 Here is another sample output. This time user supplies an integer that is greater than 100. Enter an integer which is not greater than 100:102 Custom exception is raised.Error Details:You have made the integer greater than 100. Here is another sample output. This time user does not supply an integer. Enter an integer which is not greater than 100:abc Here is the error Details:invalid literal for int() with base 10: 'abc' Debugging Code Using assert Statement The assert statements help you debug your code. Using the assert keyword, you can test a condition. If the condition is false, the program raises an AssertionError . Here is a simple example for you. Demonstration 6 In this example, I have a list called my_list . It contains 4 elements. Now I test whether the length of the list is greater than 5. This condition returns False . Since I have used the assert statement, an AssertionError is raised. It also displays the supporting message in the output. This supporting message is optional for you. my_list = [1, 2, 3, 4] #If the condition is False, AssertionError is raised. assert len(my_list) >= 5, "List length is less than 5." Output Here is a sample output. Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter8/using_assert.py", line 3, in <module> assert len(my_list) >= 5, "List length is less than 5." AssertionError: List length is less than 5. Demonstration 7 You can use try-except to handle this exception. Otherwise, you have seen that the program will halt and produce a traceback. The following code segment shows the use of try-except to handle the AssertionError . # using_assert.py my_list = [1, 2, 3, 4] try: assert len(my_list) >= 5, "List length is less than 5." except AssertionError as e: print(f"Error details: {e}") Output Here is the output. Error details: List length is less than 5. Analysis In demonstration 6 and demonstration 7, I assume that system variable __debug__ is True by default. You can turn it off. For example, you can use -O (O is in the capital) or -OO in the command line. The -O flag turns on the basic optimization. The -OO discards docstrings too, besides basic optimizations. Here is a sample: Since 2 is always less than 3, you get the AssertionError if you write : >>> assert 2>3 Here is the output. Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError Now I use -O flag. C:\Users\Vaskaran Sarcar>python -O Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> assert 2>3 >>> Notice that you do not see the AssertionError this time. There is an alternative way. You can set an environment variable to set this flag. Here is a sample. I do this in a Windows10 system. C:\Users\Vaskaran Sarcar>SET PYTHONOPTIMIZE=TRUE C:\Users\Vaskaran Sarcar>python Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> assert 2>3 >>> Again, you do not see the AssertionError . It is because you made it off already. You can reset it back. Here is the sample: >>> exit() C:\Users\Vaskaran Sarcar>SET PYTHONOPTIMIZE= C:\Users\Vaskaran Sarcar>python Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> assert 2>3 Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError I hope that you have enjoyed this chapter. Now it’s time for exercises and projects. EXERCISE E8.1 Predict the output when you run the following code. try: result = 15/0 except ArithmeticError: print("Caught the ArithmeticError.Your divisor is zero.") except ZeroDivisionError: print("Caught ZeroDivisionError.Correct the divisor which is zero at present.") E8.2 Predict the output my_string="Hello" assert my_string== "Hi", "You should use 'Hi'" E8.3 Predict the output try: raise BaseException('BaseException raised.') except Exception as e: print(f"Error Details:{e}") E8.4 Write a program in which a user can continue entering integers. The program should continue, even if the user provides an invalid input. The user can type ‘q’ to quit the program. Solution to Exercises E8.1 Caught the ArithmeticError.Your divisor is zero. Explanation: ArithmeticError is a built-in exception. It can handle the exceptions when the code raises an arithmetic error. The built-in exceptions like ZeroDivisionErro r, FloatingPointError also belong to this category. For example, you can think of a Vehicle as the base class for both Bus and Train . So, when you talk about a bus or train, you are talking about a specific vehicle. Similarly, ZeroDivisonError class is a specific type of ArithmeticError class. You may not know the concept of class/object at this moment. But you can know a simple fact. It says that both ArithmeticError and ZeroDivisionError could handle the error. But I placed AirthmeticError before ZeroDivisonError . So, we catch the error inside the ArithmeticError block. If you change their order, as follows: try: result = 15/0 except ZeroDivisionError: print("Caught ZeroDivisionError.Correct the divisor which is zero at present.") except ArithmeticError: print("Caught the ArithmeticError.Your divisor is zero.") You can see a different output: Caught ZeroDivisionError.Correct the divisor which is zero at present. I have shown you how to arrange multiple except blocks in your code. You can read that section again. E8.2 This code segment raises the AssertionError if my_string is not Hi.( I assume that system variable __debug__ is True in this case) >>> my_string="Hello" >>> assert my_string== "Hi", "You should use 'Hi'" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: You should use 'Hi' E8.3 This catch block does not catch BaseException . It is because BaseException is the parent class of Exception . Here is a sample output: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter8/exercise8.3.py", line 2, in <module> raise BaseException("BaseException raised.") BaseException: BaseException raised. E8.4 Here is a sample program. print("Exercise-8.4") print("*" * 20) def test_me(): flag = True while flag: user_input = input("Keep entering a valid integer.(Type q to quit):") if user_input == 'q': break try: display_me = int(user_input) print(f"Correct.You entered:{display_me}") except Exception as e: print(f"Error:{e}") #This statement is placed outside the while loop print("End of the exercise.") test_me() Here is a sample output: Exercise-8.4 ******************** Keep entering a valid integer.(Type q to quit):12 Correct.You entered:12 Keep entering a valid integer.(Type q to quit):342 Correct.You entered:342 Keep entering a valid integer.(Type q to quit):-5 Correct.You entered:-5 Keep entering a valid integer.(Type q to quit):2.3 Error:invalid literal for int() with base 10: '2.3' Keep entering a valid integer.(Type q to quit):abc Error:invalid literal for int() with base 10: 'abc' Keep entering a valid integer.(Type q to quit):56 Correct.You entered:56 Keep entering a valid integer.(Type q to quit):q End of the exercise. CS 6.1 Implementation Here is the solution to CS 6.1. You can consider it as an improved solution for CS 5.1 because it can report a wide range of invalid inputs. print("=" * 25) print("This is a simple calculator.") print("It supports the following operations:") print("i)Addition" "\nii)Subtraction" "\niii)Multiplication and " "\niv)Division.") print("=" * 25) valid_operators = ["+", "-", "*", "/"] def add_numbers(num1, num2): """ Adds the numbers. """ return num1 + num2 def subtract_numbers(num1, num2): """ Subtracts the numbers. """ return num1 - num2 def multiply_numbers(num1, num2): """ Multiplies the numbers. """ return num1 * num2 def divide_numbers(num1, num2): """ Divide num1 by num2. """ return num1 / num2 def compute(num1, operator, num2): """ This function computes the final result. """ result = 0 #default value if operator == '+': result = add_numbers(num1, num2) elif operator == '-': result = subtract_numbers(num1, num2) elif operator == '*': result = multiply_numbers(num1, num2) #elif operator == "/": else: result = divide_numbers(num1, num2) print(f"The final result is:{result}") def main(): """ This is the top-level function. It calls the compute() function. """ try: usr_input1 = input("Enter the first number:") first_number = float(usr_input1) usr_input2 = input("Enter the next number:") second_number = float(usr_input2) usr_opr = input("Enter an operator(+,-,*,/): ") if usr_opr not in valid_operators: raise Exception("Invalid operator.") compute(first_number, usr_opr, second_number) except ZeroDivisionError as e: print(f"Invalid Operation.Details: {e}") except ValueError as e: print(f"Invalid input.Details: {e}") except Exception as e: print(f"Error details: {e}") main() Case Study VII CS 7.1 Problem Statement This time you make a report card of students. Assume that a student joins a beginner’s course in Python. To get a grade card, a student needs to meet the following criteria: He needs to submit two assignments (50 marks each) before he appears in the final examination (which is 100 marks). You calculate the final score based on these assignment scores and the final examination score. Consider 25% of total marks in assignment and 75% marks in the final examination to prepare the grade card. When a student scores more than 90, he gets A+, which means Outstanding. If he scores more than 80, he gets A, which means Very Good. If the score is 70 or above, he gets B, which means Good. Consider a score that is less than 70 as Fail. Try to manage your application in a better way. For example, if any of the assignment scores is greater than 50, your application should report an error. The same rule applies if anyone inputs a final examination score greater than 100. You save these student records in text files, which have names like student_name.txt. You store these files in a separate folder. You can name it GradeScores. I give you three sample outputs to understand it better. Here is the sample-1. A user enters the valid inputs. Enter the student name:Ravi S Assignment-1 score:25.5 Assignment-2 score:34.5 Exam score:87 Final score:80.25 Grade: A(Very Good) The current working directory: E:\MyPrograms\Python\PythonCrashCourse\Projects Get the report card at: E:\MyPrograms\Python\PythonCrashCourse\Projects\GradeScores\Ravi S.txt Here is a sample report card that you store. The content of the text file Ravi S.txt may look like the following: ***Report Card*** ***Course name: Python for Beginners*** ================================================== Student Name:Ravi S Assignment-1 Score:25.5 Assignment-2 Score:34.5 Exam Score:87.0 Final score:80.25 Grade/Remark: A(Very Good) Here is the sample-2. A user enters an assignment score that is above 50: Enter the student name:John Assignment-1 score:23 Assignment-2 score:51.5 Error: Assignment2 score cannot be greater than 50. Provide the correct input next time! Here is the sample-3. A user enters the final examination score that is above 100: Enter the student name:Kate Assignment-1 score:23 Assignment-2 score:45.3 Exam score:105 Error: Final exam score cannot be greater than 100. Provide the correct input next time! Chapter 9: Programming with Files The first question that may come to your mind, why do you need a file? There is a simple answer to this question. If you process an enormous amount of data, supplying inputs one-by-one is not a pleasant idea. Also, taking one input at a time from the user makes the program execution slow. So, a better approach is you store all these inputs in a file. Then you let your program read the date from it directly. Besides, it is useful to write a detailed log into a file for further processing. This chapter discusses these kinds of basic operations. Reading from a File Let us read from a text file. I create a text file and save it as OriginalFile.txt in my preferred location C:\TestData. Here is the content of the file: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. Now I have the file; so, I can read its content using a Python program. In the upcoming demonstration, I show you two different approaches. You can use them to read the data from a file. The first one is easy to understand, but not convenient. The second one is a better approach where I use a for loop to read the content of the file. Before you see the demonstration, read the following content. Quick Talk about File Paths Experts suggest that we organize the code properly. The current working directory and the files you use -rarely reside in the same directory. So understanding the file paths is important. If the python program and the files you use, stay at the same location, you do not need to mention the file location. In my case, they stay at different locations. So, I mention the full path of a saved file, called OriginalFile.txt. I use the double backslashes (\\) inside the open() function as follows: 'C:\\TestData\\OriginalFile.txt' It’s worth remembering that we use single backslashes are for escape characters in Python. POINT TO REMEMBER Windows systems use the backslash (\) instead of a forward slash (/) to display file paths. But you can use forward slashes too in your program. You may see the use of both absolute paths and relative paths. Absolute paths talk about the exact location of a file. For example, in my windows system, C:\TestData\AnotherDirectory is an example of an absolute path. Relative paths talk about the position relative to some other place in a file system. For example, you can use TestData\AnotherDirectory as a relative path. Developers often append a relative path to an absolute path to form a new absolute path. Consider an example. Suppose you append AnotherDirectory\SubDirectory to the absolute path C:\TestData . This activity gives you get a new absolute path C:\TestData\AnotherDirectory\SubDirectory The directory location in which your Python program resides is your current working directory. To know about a current working directory, you can use the following code: import os print(f"The current working directory is: {os.getcwd()}") For example, when I run this code, I get the following output: The current working directory is: E:\MyPrograms\Python\PythonCrashCourse\chapter9 Now I can append a relative path to this location to make a new absolute path. The following code segment shows how to retrieve a current working directory before you append a relative path to it. #get_current_working_directory.py import os current_path = os.getcwd() print("The current working directory is as follows:") print(current_path) relative_path = "AnotherDirectory\\SubDirectory" new_path = os.path.join(os.getcwd(),relative_path) print("The new path is as follows:") print(new_path) I ran this program and got the following output: The current working directory is as follows: E:\MyPrograms\Python\PythonCrashCourse\chapter9 The new path is as follows: E:\MyPrograms\Python\PythonCrashCourse\chapter9\AnotherDirectory\SubDirectory I hope you have got the difference between an absolute path and a relative path now. Let us concentrate on the upcoming demonstration now. In the upcoming program, I use the following line: file_object = open('C:\\TestData\\OriginalFile.txt','r') Notice that I have passed two arguments inside the open() function. I use these arguments to mention the file location and the mode of the operation. The open() function returns an object which I store in file_object . I use this file_object for further processing. The second argument is optional. In this chapter, I’ve used the following modes. These are very common. ‘r’ : used for reading. The file pointer stays at the beginning of the file. ‘w’ : used for writing. If the file doesn’t exist, Python creates the file for you. Otherwise, it deletes the contents of the existing file. ‘a’ : used to append the data at the end of the file. The file pointer stays at the end of the file. If the file doesn’t exist, Python creates the file for you. ‘r+’ : used for both reading and writing. The file pointer stays at the beginning of the file. In demonstration4, I use rb and wb modes as well. These are like r and w modes, but the only difference is that you use them for binary files. In other people’s code, you can see the use of w+, and a+ modes as well. These have some special capabilities. For example, if the file does not exist, ‘ a ’ mode creates a file for writing, but you can use ‘a+’ mode both for reading and writing. In short, you use ‘+ ’ for both reading and writing. Apart from these common modes, you can see ‘ x ’ for exclusive creation (if file exists), ‘t’ for text mode. For backward compatibility, there is ‘U’ mode. You can have a quick look at https://docs.python.org/3.3/library/functions.html#open to know more about them. You can remember these modes easily when you use them in your code. Next, you see the following code segment: print("Reading each line one by one:") first_line = file_object.readline() second_line = file_object.readline() third_line = file_object.readline() This code segment shows that using the function readline(), you can read a complete line from a file. Inside OriginalFile.txt, there are three lines. So, I use readline() three times to print the content. Before I move to approach2, I want you to note that readline() inserts a line break after each line. So, when you use this function in your output, you can notice a line gap between these lines. Here is the sample: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. Now notice the use of the keyword argument end . It becomes available in Python3. You saw an example of this function in Chapter 5 as well. The end= tells what you should print after a function processes other argument(s). For example, if I write the following code: file_object = open('C:\\TestData\\OriginalFile.txt', 'r') for current_line in file_object: print(current_line, end='**') I’ll get the following output: Dear Reader, this is my test file. **It is originally stored at C:\TestData in my system. **I hope you'll enjoy learning. ** In the upcoming example, I use the following code: for current_line in file_object: print(current_line, end='') If you execute this code segment, you notice the following output without the line breaks: Reading entire file using for loop: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. I show you another approach. Here I read the entire content of the file using the read() method. This method can read the entire content of the file and store it in a long string. Finally, I print the content. So, you see the following code in approach3: file_content = file_object.read() print(file_content) Now go through the following demonstration and output. Demonstration 1 Here is the complete program. # reading_text_file_example_1.py file_object = open('C:\\TestData\\OriginalFile.txt', 'r') print("---Approach:1---") print("Reading each line one by one:") first_line = file_object.readline() second_line = file_object.readline() third_line = file_object.readline() print(first_line) print(second_line) print(third_line) #print(first_line, end='') #print(second_line, end='') #print(third_line, end='') file_object.close() print("---Approach:2---") print("Reading entire file using for loop:") file_object = open('C:\\TestData\\OriginalFile.txt', 'r') for current_line in file_object: print(current_line, end='') file_object.close() print("---Approach:3---") file_object = open('C:\\TestData\\OriginalFile.txt', 'r') print("Using the read() method.") file_content = file_object.read() print(file_content) Output Here is the output. ---Approach:1--Reading each line one by one: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. ---Approach:2--Reading entire file using for loop: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. ---Approach:3--Using the read() method. Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. Analysis From the output, you can see that in approach1, you get a line break between the lines. It is because readline() inserts a line break after each line. But now you know the use of keyword end ; so, if you write the following segment: print(first_line, end='') print(second_line, end='') print(third_line, end='') You’ll not see the line breaks between the lines in the output. POINTS TO REMEMBER I use open() and close() in the demonstration1. You know that I use them to open the file and close the file. This approach suffers from a potential drawback. If there is a bug that prevents the close() method to execute, the file may never close. In that case, the data can be lost or corrupted. Also, as a precautionary step, if you close the file early, you may work with a closed file. It also causes unexpected results. So, if you are unsure when to close a file, it’s a better practice to leave the decision to Python. You can use the ‘with’ keyword in this context. You’ll see a demonstration on this topic later in this chapter. Once you learn this approach, I recommend you to follow the same in your code. Writing to a File In demonstration1, you see how to open and read from a text file. In this section, let’s write something to the file. In the upcoming example, I use the same file, OriginalFile.txt. This time I append a few more lines into it. Demonstration 2 Here is the program for you. # writing_to_file.py # Opening the file in 'a' mode my_file = open("C:\\TestData\\OriginalFile.txt", "a") my_file.write("This is a new line.") my_file.write("\nThis is another new line.I append this too.") my_file.close() Output After you execute this program, go to the file location and open the file to verify the result. Here is the output. Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. This is a new line. This is another new line.I append this too. Analysis Each time you execute the program, these lines are added. Notice that I’ve added the escape character \n before the last line, which I print. So, you see this line starts from a new line. Alternative Way to Read and Write Demonstration1 and demonstration2 show you how to open a file, read from a file, write to a file, and finally close the files. In the approach3 of demonstration1, you saw the usage of the read() method. It has an optional parameter to show the buffer size. By default, it is -1 , which means the entire file. So, in that example, I did not pass any argument inside this method. In the upcoming demonstration, you see an alternative usage of it. Here I specify the buffer size. This is useful when you care about memory resources. Consider a case: If you load the entire content of an extensive file at once, you can face a memory shortage problem. In the upcoming example, I use the same file, OriginalFile.txt to read the data from it. But this time I specify the buffer size. So, you see the following line of code: content=input_file.read(15) It tells you that I read 15 bytes at a time. To verify this, while writing, I use the following line of code: output_file.write(content+ "\n") So, when you get the output, you see that each line contains a maximum of 15 characters. If you remove the escape character \n while writing the lines in the output file, you see no difference between the input and output files. Demonstration 3 The previous section describes the most important characteristic of this program. The remaining code is easy to understand. In brief, I want you to note the following points: I read from a file and write to a different file. So, I need two files to open and close. The input_file represents my input file, and the output_file represents my output file. I could use r+ mode here. It is because, in the specified location, I already had a file called OutputFile.txt . If in your case, there is no such file, you’ll encounter errors something like the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/Chapter9/reading_writing_using_buffer_size line 3, in <module> output_file= open('C:\\TestData\\OutputFile.txt','r+') #error if the file doesn't exist FileNotFoundError: [Errno 2] No such file or directory: 'C:\\TestData\\OutputFile.txt' In that case, I suggest you using w mode (as shown in the commented line). You have already seen the use of r and w mode. So, this time I wanted you to show the use of r+ mode. This is why I use this mode in this program. Here is the complete program for you. # reading_writing_using_buffer_size.py # Opening two files-one for reading and one for writing input_file = open('C:\\TestData\\OriginalFile.txt', 'r') # Will NOT work if the file does not exist output_file= open('C:\\TestData\\OutputFile.txt','r+') # Will work even if the file does not exist #output_file = open('C:\\TestData\\OutputFile.txt', 'w') content = input_file.read(15) while len(content): output_file.write(content) #output_file.write(content + "\n") content = input_file.read(15) input_file.close() output_file.close() Output Execute this program. Then go to the output file location and open the file to verify the result. Here is the output. Dear Reader,thi s is my test fi le. It is origi nally stored at C:\TestData in my system. Ih ope you'll enjo y the learning. Analysis As said before, if you avoid the newline character (\n) and use the following line: output_file.write(content) instead of using the following line: output_file.write(content+ "\n") you can see the same content as in the input file which is as follows: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. You may not notice the changes made to a file until you close it. So, I suggest that you close the file before you make any changes to it. Working with Binary Files Till now I was working with simple text files. Let us now work with a binary file. A binary file is a non-text file such as images or videos. To do reading from a binary file, I store a file called MyImage.png inside the location C:\TestData In the upcoming example, you’ll see the following lines of code: input_file = open("C:\\TestData\\MyImage.png", "rb") output_file = open("C:\\TestData\\OutputImage.png", "wb") This segment is almost like the previous demonstration. The only difference is that this time I’ve used ‘ rb’ and ‘ wb’ instead of ‘r’ and ‘w’ (or ‘r+’ ). Also, no escape character is present when I write to the output file. Demonstration 4 Here is the complete program for you. # working_with_binary_files.py # Opening two files-one for reading and one for writing input_file = open("C:\\TestData\\MyImage.png", "rb") output_file = open("C:\\TestData\\OutputImage.png", "wb") content = input_file.read(15) while len(content): output_file.write(content) content = input_file.read(15) input_file.close() output_file.close() Output Execute the program and go to the output file location. You can see a new image called OutputImage.png which is a duplicate of the original image. I’m taking a partial snapshot from my machine to show this: Figure 9-1: OutputImage.png is created inside c:\TestData Renaming and Removing Files You can rename and remove an existing file. The following program can show this. In the previous program, you’ve created OutputImage.png . Now you can rename it using the rename function. This function has the following format: rename(‘old_file_name’,’new_file_name’) . To delete a file, you can use the remove() function which has the following format: remove(“file_name”). Before you see them in demonstration 5, go through the following notes: In the following program, I rename the file first. Then I delete the file. So, you see the use of rename before remove . To use the functions, rename, and remove , I needed to use the following line at the beginning: from os import rename, remove Demonstration 5 Here is the complete program for you. # rename_and_remove_file.py from os import rename, remove #Renaming the image rename("C:\\TestData\\OutputImage.png","C:\\TestData\\OutputImage1.png") #Deleting the file remove("C:\\TestData\\OutputImage1.png") Output After you execute this program, go to the output file location. This time you do not see any file called OutputImage.png or OutputImage1.png. It is because in the first step; I rename OutputImage.png to OutputImage1.png. Later, I delete this file. If you disable the code for the deletion operation, you can see OutputImage1.png in this location. Using with Keyword Closing a file is an important activity. When you close a file, you free system resources. This activity also allows other code to use this file now. If you forget to close one small file, you may not see the immediate effect. But if you have too many open files, you may see the impact of memory leaks, which may end your program abnormally. This is why I told you earlier that you should close the file properly to avoid unwanted situations. If you are unsure when to close a file, you can leave the decision to Python. The ‘with’ keyword in this context can serve this purpose. Demonstration 6 First, I rewrite “Approach1” in demonstration1. It is as follows: # using_with_keyword.py # Using the ‘with’ keyword in this example. # Python closes the file when it is no longer needed. with open('C:\\TestData\\OriginalFile.txt', 'r') as file_object: print("---Approach:1---") print("Reading each line one by one:") first_line = file_object.readline() second_line = file_object.readline() third_line = file_object.readline() print(first_line, end='') print(second_line, end='') print(third_line, end='') Output for Approach1 ---Approach:1--Reading each line one by one: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. Now I rewrite “Approach2” in demonstration1. It is as follows: # Using the ‘with’ keyword in this example. # Python closes the file when it is no longer needed. with open('C:\\TestData\\OriginalFile.txt', 'r') as file_object: print("---Approach:2---") print("Reading entire file using for loop:") for current_line in file_object: print(current_line, end='') Output for Approach2 ---Approach:2--Reading entire file using for loop: Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. Now I rewrite “Approach3” in demonstration1. It is as follows: # Using the ‘with’ keyword in this example. # Python closes the file when it is no longer needed. with open('C:\\TestData\\OriginalFile.txt', 'r') as file_object: print("---Approach:3---") print("Using the read() method.") file_content = file_object.read() print(file_content) Output for Approach3 ---Approach:3--Using the read() method. Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. I hope you'll enjoy learning. I hope you have got an idea about how to use the ‘with’ keyword in your program. Handling FileNotFoundError In Chapter 8, you learned how to manage run-time errors or exceptions. You can use the knowledge to make a better program. When you work with files, handling missing files is a common activity. Your program may not find a file for various reasons. Here are some of them: The file resides in a different location, You misspelled the file name, The intended file does not exist at all, etc. So, it’s a better practice to put the code in the try-except block to avoid unexpected outcomes. Demonstration 7 Here I change the approach3 of the previous demonstration to give you an idea about it. In this example, I use the file name OriginalFile_1 which does not exist inside C:\TestData on my computer. # Using try-except to handle FileNotFoundError try: my_file = 'C:\\TestData\\OriginalFile_1.txt' with open(my_file, 'r') as file_object: print("Using the read() method.") file_content = file_object.read() print(file_content) except FileNotFoundError as e: print(f"The file {my_file} is not found.") print(f"Error details:{e}") Output Here is the output: The file C:\TestData\OriginalFile_1.txt is not found. Error details:[Errno 2] No such file or directory: 'C:\\TestData\\OriginalFile_1.txt' Analysis If you do not handle this error using try-except , you can see the following error: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter9/handling_file_not_found_error.py" line 4, in <module> with open(my_file, 'r') as file_object: FileNotFoundError: [Errno 2] No such file or directory: 'C:\\TestData\\OriginalFile_1.txt' EXERCISE E9.1 Write a program to print the content of a file. E9.2 Write a program to copy an image. E9.3 Write a program to rename a file. E9.4 Write a program to remove a file from a specific location. E9.5 Write a program to count an approximate number of words in a text file. E9.6 Write a program to print first ‘n’ lines from a file. A user can supply the value of ‘n’. Solution to Exercises E9.1, E9.2, E9.3, E9.4 I already showed you the possible solutions to these exercises in this chapter. This time I want you to write better programs. For example, you can write a small function to make a solution for E9.1. You can use your knowledge of exception management in your implementation. Here, I show you a sample solution for E9.1 def print_file_content(input_file): """ This function can print the content of a text file.""" try: with open(input_file,"r") as file_object: file_content = file_object.read() except FileNotFoundError as e: print(f"The file {input_file} is not found.") print(f"Error details:{e}") else: print(file_content) print_file_content('C:\\TestData\\OriginalFile.txt') Author’s comment: You see an else block here. You understand that this block executes only if the code in the try block passes well. E9.5 def count_words(input_file): """ This function counts the approximate number of words in a text file. """ try: with open(input_file,"r") as file_object: file_content = file_object.read() except FileNotFoundError as e: print(f"The file {input_file} is not found.") print(f"Error details:{e}") else: separate_words = file_content.split() word_count = len(separate_words) print("The content of the file:") print("-" * 20) print(file_content) print("-"*20) print(f" The file has {word_count} words (approx).") count_words('C:\\TestData\\sample_text_file.txt') Sample output: The content of the file: -------------------Sky is blue. Apple is red. Sam is a good boy. -------------------The file has 11 words (approx). Author’s comment: You can enhance this solution. For example, you can ask the filename of a user. I left this exercise for you. You can make an alternative solution too. For example, you can use a for loop instead of using the len() function. Here is a sample: #word_count = len(separate_words) # Alternative solution word_count = 0 for word in separate_words: word_count += 1 #remaining code. E9.6 I use the OriginalFile.txt for this example. def print_lines(input_file, lines): """ This function prints first ‘n’ lines from a file.""" try: with open(input_file, "r") as file_object: count = 0 while count < lines: line_content = file_object.readline() print(line_content,end='') count += 1 except FileNotFoundError as ex: print(f"The file {input_file} is not found.") print(f"Error details:{ex}") try: user_input = input('Enter how many lines you want to print from the file? ') number_of_lines = int(user_input) print_lines('C:\\TestData\\OriginalFile.txt',number_of_lines) except ValueError as e: print("Invalid input! Provide a correct input next time!") print(f"Error details:{e}") except Exception as e: print("An unknown error occurred.") print(f"Error details:{e}") Here is a sample output: Enter how many lines you want to print from the file? 2 Dear Reader, this is my test file. It is originally stored at C:\TestData in my system. Here is another sample output: Enter how many lines you want to print from the file? 2.3 Invalid input! Provide correct input next time! Error details:invalid literal for int() with base 10: '2.3' Author’s comment: You can make a better solution. For example, you can ask the file name too from a user. I left this exercise for you. CS 7.1 Implementation In this implementation, you see the use of the makedirs() function that comes from the os module. Here is the function documentation from PyCharm IDE for your immediate reference: def makedirs(name, mode=0o777, exist_ok=False): """makedirs(name [, mode=0o777][, exist_ok=False]) Super-mkdir; create a leaf directory and all intermediate ones. Works like mkdir, except that any intermediate path segment (not just the rightmost)will be created if it does not exist. If the target directory already exists, raise an OSError if exist_ok is False. Otherwise, no exception is raised. This is recursive. """ This is why I import the os module at the beginning of this implementation. Go through the function documentation to understand the usage of different functions. Here is the complete implementation for you: import os class ScoreExceedsError(Exception): """ This is a custom exception.""" pass def calculate_grade(a1_score, a2_score, e_score): """ This function calculates the final score. Here is the consideration: 25% of the total marks in assignment and 75% of the total marks make the grade card. """ final_score = (a1_score + a2_score) * .25\ + e_score * .75 return final_score def make_grade(score): """ This function makes the grade.""" grade = "" if score > 90: grade = "A+(Outstanding)" elif score > 80: grade = "A(Very Good)" elif score >= 70: grade = "B(Good)" else: grade = "F(Fail)" return grade def save_scores(name, assign1, assign2, exam, score, grade): """ This function stores the result in a text file. It picks the current working directory.Then create a directory(if it does not exist),called GradeScores. All records are stored as test files inside this directory. """ try: # Retrieve the current working directory current_path = os.getcwd() print("The current working directory:") print(current_path) # Adding a relative path to the current path relative_path = "GradeScores" new_path = os.path.join(current_path, relative_path) # Making the directory if it does not exist if not os.path.exists(new_path): os.makedirs(new_path) new_path = new_path + '\\' + name + '.txt' print("Get the report card at:") print(new_path) with open(new_path,'w') as file_object: file_object.write("***Report Card***") file_object.write("\n***Course name: Python for Beginners***\n") file_object.write("=" * 50) file_object.write(f"\nStudent Name: {name}") file_object.write(f"\nAssignment-1 Score:{assign1}") file_object.write(f"\nAssignment-2 Score:{assign2}") file_object.write(f"\nExam Score:{exam}") file_object.write(f"\nFinal score:{score}") file_object.write(f"\nGrade/Remark: {grade}") except FileNotFoundError as e: print(f"The file is missing.Details:{e}") except Exception as e: print(f"Error details: {e}") def main(): """ It is the top level function for this application. """ try: student_name = input("Enter the student name:") assign1 = float(input("Assignment-1 score:")) if assign1 > 50: raise ScoreExceedsError("Assignment1 score cannot be greater than 50.") assign2 = float(input("Assignment-2 score:")) if assign2 > 50: raise ScoreExceedsError("Assignment2 score cannot be greater than 50.") exam = float(input("Exam score:")) if exam > 100: raise ScoreExceedsError("Final exam score cannot be greater than 100.") except ValueError as e: print(f"Invalid input.Details:{e}") except ScoreExceedsError as e: print(f"Error: {e}") print("Provide the correct input next time!") except Exception as e: print(f"Error details: {e}") else: final_score = calculate_grade(assign1,assign2,exam) print(f"Final score:{final_score}") grade_score = make_grade(final_score) print(f"Grade: {grade_score}") save_scores(student_name, assign1, assign2, exam, final_score, grade_score) main() Chapter 10: Object-Oriented Programming Concepts Welcome to the basics of object-oriented programming (OOP). I remind you that Python supports both procedural programming and object-oriented programming. The OOP in Python is not exactly like other languages like Java, C#, etc. There are some differences. Those differences are not your focus in this chapter. Instead, to get the most benefit, here you’ll learn the basic terms in OOP. These are independent of any programming language. In the next chapter, you’ll see how some of these concepts can be implemented in Python. Let us begin with some fundamental questions and answers. For example, why do you need this kind of programming? Or how these concepts can make your life easy? If you know the answers, the learning path will be easy, and you can relate these concepts in various ways. But before you start, there are two warning messages for you: Do not lose your motivation if you cannot understand everything after the first pass. Sometimes it is complex, but gradually it will be easier for you. Many developers criticize the concepts of OOP. But you remember that the human minds are not always comfortable with new concepts or ideas. So, before you criticize these concepts, understand and use them in various applications. Finally, make your decisionwhether to appreciate or criticize. Now let us begin the journey… Computer programming started with binary code. We used mechanical switches to load the programs. It is easy to assume that a programmer’s life was very challenging in those days. To make their lives easy, some high-level programming languages were developed. In those languages, we began to use some simple English-like instructions. Remember that a computer can understand instructions in a binary language only. So, the compiler’s job was to translate these English-like instructions into binaries. Eventually, these high-level languages gained in popularity. Over a period, computer capacity and capabilities increased a lot. Then developers started developing complex applications. Unfortunately, none of the programming languages were mature enough to handle them effectively. Some primary concerns were as follows: How can I avoid duplicate efforts? Or, how can you reuse the existing code? How can I control the use of global variables in a shared environment? How can I debug the code when too much jumping between levels occurs in a program? (For example, a C programmer can use a goto statement to make an unconditional jump in his applications.) How can I make a new engineer’s life easier? How can I maintain a large codebase in a better way? To solve these problems, expert programmers made some changes. They started breaking the large problems into smaller problems. The idea behind this philosophy was very simple. It states that if you can solve these smaller problems, eventually you’ll solve the big problem. So, they started portioning the big problems into small chunks. And the concept of functions (or procedure or subroutines) was developed. They dedicated each function to solve one small problem. So, managing these functions and the interactions among them became the key focus and it created the concept of structured programming. Structured programming was a big hit. It is because managing small functions is easy and you can debug them easily. At the same time, developers also started limiting the use of global variables. They started replacing them with local variables in the functions wherever possible. Structured programming was popular for almost two decades. During this time, the capacity of hardware increased a lot. So, developers wanted to solve more complex tasks. Over the period, the limitations of structured programming became more prominent. For example, consider the following cases: Suppose, in a program, you have used a particular data type across many functions. Later you discover that you need to change the data type. As a result, you need to make changes in all functions in this application. The key component of structured programming is data and functions. It is difficult to model all real-world scenarios with them. In the real world, whenever you create a product, there are two areas you need to focus on. Purpose. Why do you need the product? Behavior. How can the product make your life easier? Then the idea of objects came into existence. It will be good to know that Alan Curtis Kay is widely considered the one father of object-oriented programming. He used this naming along with some colleagues at Palo Alto Research Center (PARC). It was formerly known as Xerox PARC. POINTS TO REMEMBER Structured programming makes a program more efficient by enforcing a logical structure. You can do this type of programming in different programming languages. It also relates to the basis of structured programming. Here you see that functions (or procedures) play a vital role in the data. For simplicity, you can think of structured programming without object-oriented functionalities/features as procedural programming. So, the fundamental difference between procedural (or purely structured) programming and object-oriented programming can be summarized as In object-oriented programming, instead of focusing on the operations on data, you focus on the data itself. In OOP, there are some important principles. I’ll cover them quickly in this chapter, and you will get a brief introduction to each of them. I recommend you to read and understand these concepts clearly. Class and Objects These are at the core of OOP. A class is a blueprint or the template for its objects. Objects are instances of a class. Each object has its state, behavior, and identity. In simple language, in structured programming, you segregate the problem into small functions. But in OOP, you divide the problem into objects. I assume that you are familiar with data types like int, float, etc. You know that these are primitive data types. We also call them built-in data types because they are already defined in a computer language. But when you create your data type, let’s say, Player, you need to create a Player class. When you need to create an integer variable, you mention the int first. In the same way, when you create a Player object (e.g., Michael ), you mention your Player class first. Let us look into this. Are you familiar with the game of football (or soccer, as it’s known in the United States)? Like any other game, the players are selected for their skills. These players have the least level of match fitness and some important athletic capabilities. So, when I say that Ronaldo is a footballer (a.k.a. soccer player), you can predict that Ronaldo has these basic abilities and some skills specific to football. Notice that you can make these assumptions even though Ronaldo is unknown to you). This is why you can say that Ronaldo is an object of a Footballer class. Note It may appear to you it is a chicken-or-the-egg type of dilemma. Perhaps if I say, “X is playing like Ronaldo,” then, in that case, Ronaldo is acting as a class. However, in objectoriented design, you make things simple. You ask,” Who comes first?” The answer to this question is the class in your application. Now consider another footballer, Beckham . You can predict again that if Beckham is a footballer, then he must be excellent in many aspects of football. Also, he must have a minimum fitness level to take part in a match. Now let’s assume Ronaldo and Beckham both are taking part in the same match. It is easy to predict that although both are footballers, their playing style and performance differ from each other in that match. In the same way, in the OOP world, the performing objects differ from each other, even though they belong to the same class. You can consider different domains. For example, you can say that a Dog is an object from a Mammal class, a Bus is an object from a Vehicle class, and so on. In short, in the real-world scenario, each of the objects must have two basic characteristics: state and behavior. Consider the objects—Ronaldo or Beckham from the Footballer class again. You may notice that they have states like “playing state” or “non-playing state.” In the playing state, they can show different skills (or behaviors)—they can run, they can kick, they can pass the ball, and so forth. In a non-playing state, the behavior will also change. In this state, they can take a much-needed nap, or they can eat their meals. Or they can simply relax by doing activities like reading a book, watching a movie, and so forth. In the same way, you can also say that the televisions in your home, at any moment, can be an “on” state or an “off” state. It can display different channels if, and only if, it is in switched on mode. It shows nothing if it is in switched off mode. So, to begin with, in object-oriented programming, you can ask the following questions: What are the possible states of my objects? What are the different functions (behaviors) that they can perform in those states? Once you get the answers to these questions, you are ready to proceed. Software objects follow the same pattern in any object-oriented program. We store their states in fields (variables). We describe their capabilities (behaviors) through different methods (or functions). Encapsulation In OOP, you do not allow your data to flow freely inside the system. Instead, you wrap the data and functions into a single unit (i.e., in a class). It is important to note that you often see the term ‘method’ instead of ‘function’. In the OOP context, functions are often referred to as methods. Though some technical guys are very particular about these terms. They say when you invoke a function through an object of the class, you should mention it as a method, but not a function. Otherwise, you can mention it as a function. The purpose of encapsulation is at least one of the following: Putting restrictions so that the components of an object cannot be accessed directly. Binding the data with methods that will act on that data (i.e., forming a capsule) In some OOP languages, the hiding of the information is not implemented by default. So, they come up with an additional term called information hiding. Later you see that data encapsulation is one of the key features in a class. If you want to promote security, your data should not be visible from the outside world. Only through the methods defined inside the class, you can access these data. Thus, you can think of these methods as the interface between the objects’ data and the outside world (i.e., your program). In short, encapsulation is a process through which you bundle your data and associated operations (i.e., functions or methods) that can work on these data. At the same time, you define the way to access these data. Abstraction Abstraction shows only essential features and hiding the background details of implementation. It is very much related to encapsulation, but there is a difference. Let us understand it using a simple day-to-day scenario. When you press a button on the remote control of your TV, you do not worry about the internal circuits of the TV or how the remote control works. You know that different buttons on the remote control have different functionalities. As long as they work, you are happy. So, a user is isolated from the complex implementation details. These details are encapsulated within the remote control (and TV). At the same time, the common operations that can be performed through remote control can be thought of as an abstraction in the remote control. A manufacturer can further enhance this feature when the same remote can also perform on a different model or product. For example, you can use a DVD player remote control to control the volume of a TV. As a developer, you need to filter out which information is essential for you. Let us assume you make an application for an organization. When you analyze the information, you may see that you can have access to the employee name, his (or her) identification number, address, contact details, age, hobbies, spouse name, dependent name, previous experience details, etc. But when you create an Employee class for your application, you may find that we do not need all these details. Instead, you can simply include the employee name, the identification number, and the contact address in an Employee class. We term the way you filter out the unwanted information and pick the essential information as an abstraction process. Inheritance Whenever we talk about reusability, normally we refer to inheritance. It is a process in which one object acquires the properties of another object. Consider an example. You use vehicles for transportation. Now you know that Bus is one type of Vehicle because it fulfills the basic criteria of a Vehicle . Similarly, Train is another type of Vehicle . In the same way through a ‘ Goods train ’ and a ‘ Passenger train’ are different but both of them fulfill the basic criteria of a Train , which in turn is a Vehicle . So, we say that you can support hierarchical classifications using inheritance. Using inheritance, you create a new child class from an existing class. You refer to the existing class as a parent class. A parent class is also referred to with different names. For example, a C# programmer often calls a parent class a base class. But a Java programmer refers to it as a superclass. In short, you place a parent class one level up than a derived class in a hierarchical chain. Then you can add or change the functionalities (methods) inside a derived class. When you do this, you say “I’m overriding the functionalities into the child class”. Keep in mind that these modifications should not change the original architecture. For example, let’s assume you have two classes called ‘ Bus’ and ‘ Vehicle’ . And you derive the Bus class from the Vehicle class. Now you add or change the functionalities in the Bus class. These modifications should not affect the original functionalities in the Vehicle class. The key advantage is that you can type less and avoid lots of duplicate codes in a program. Polymorphism Polymorphism is associated with one name with many forms. Consider the behavior of your pet dog. When it sees an unknown person, it is angry and starts barking a lot. But when it sees you, it makes different noises and behaves differently. In the coding world, you can also consider the addition method. If the operands are numbers, addition means the sum or total of the numbers. But if the operands are strings, you expect to get a concatenated string. For example, 2.5+3 = 5.5 but “Hello” +”reader?” should produce Hello, reader? Notice that in both cases, I use + operation. Polymorphism can be of two types. ● Compile-time polymorphism: The compiler can decide very early which method to invoke when you compile the program. We also refer to it as static binding or early binding. ● Runtime polymorphism: The actual method calls are resolved at runtime. At compile time, you cannot predict which method will be invoked at run-time. Let us consider an example. Suppose you generate a random number at the very first line in a program. Now you have the following logic: if the generated number is an even number, you call a method, Method1() , which prints “ Hello ”. Otherwise, you’ll call a method whose name is the same, but it prints “ Hi ”. In this case, you do not know in advance - whether you will see “Hello” or “Hi” before the program’s execution. So, we term this as dynamic binding or late binding. Now I add a Q&A session for you. This can help you to review your understanding. Q&A Session 10.1 What are the key features of object-oriented programming? Class and objects are the backbones of OOP. Apart from them, the following are the key features of object-oriented programming: ● Encapsulation ● Abstraction ● Inheritance ● Polymorphism 10.2 How is an object different from a class? A class is a template or a blueprint. Objects are made from a class. An object is an instance of a class. An object is a physical entity. You need to allocate memory for it. But class is a logical entity, and it does not allocate memory in the system. 10.3 How does an abstraction differ from an encapsulation? Abstraction focuses on the noticeable behavior of an object. But encapsulation focuses on the implementation part of that behavior. Encapsulation helps you to bundle your data. It also hides some information that you do not want to disclose to the outside world. In this context, you can refer to the example that I discuss in this chapter. 10.4 What is the key advantage associated with the inheritance mechanism? By reusing the existing code, you can save time and effort. At the same time, this mechanism helps you to avoid duplicate codes in your application. 10.5 What are the characteristics of object-oriented programming? Here are some important characteristics: Your focus is on data, not on functions. So, you divide your program into objects, not into functions. You do not allow data to flow freely. You use methods to access them. Your objects communicate through methods. The outside world cannot access the data directly. Your application can adapt to new changes easily. It promotes easy maintenance. Case Study VIII CS 8.1 Problem Statement Let us assume you need to make an application that can design a computer science course in various institutions. Here are the assumptions: The course includes three subjects. Two subjects are common across all institutions. Let us assume these two subjects are mathematics and soft-skills. But the third one is a specialized subject that can vary across the institutions. A user can supply the institution name and the third subject name when he initializes an object. But these are optional for him. By default, the institution’s name is St. Stephen College , and the specialized subject is Compiler design . To reduce the code size, you can assume there is no invalid user input to consider. I give you two sample outputs. Sample-1: A User supplies the institution name as Presidency and the compulsory third subject as Python Programming. Institution name: Presidency college. Computer science course includes: 1:Mathematics. 2:Soft-skills. 3:Python Programming ---------Sample-2: A User does not supply the institution name and the compulsory third subject. Institution name: St. Stephen college. Computer science course includes: 1:Mathematics. 2:Soft-skills. 3:Compiler design. ---------Author’s Comment: Here I use the concepts of class, object, and inheritance in OOP. You will: See the use of different constructors. Know about how to use a superclass constructor effectively. See the use of default values in your code. I follow a simple template design pattern in this project. I want you to know this name because it is a famous Gang of Four design pattern. Chapter 11: Class, Objects, and Inheritance Let us do some object-oriented programming exercise in Python. Here I make things very simple. So, I’ll ignore some typical corner cases to make your journey easy. Constructing the Building Blocks for OOP Let us recollect what you have learned in the previous chapter. Class and objects are the building blocks in OOP. To represent a real-world entity, we create a class, and then we create objects from it. So, you understand that to create objects from a class; you need to have the class first. Since we make objects from a particular class, we define the common behavior of these objects inside the class. You may note that objects and instances are often used interchangeably. It is because we refer to the process of creating an object from a class as instantiation. POINTS TO REMEMBER You start with a class, which is the architectural blueprint for you. A class defines the structure and behavior of the objects. From a single blueprint, you can create multiple buildings. Similarly, from a single class, you can construct multiple objects (or instances). (As said before, I ignore some typical corner cases when I make this statement. For example, a true singleton class cannot have multiple instances). Developers often refer to the terms function and method interchangeably. In Python, a method is usually associated with objects/classes. In other words, you use a method for a particular object. A method can access the data that is contained in a class. Classes help you create a new data type. Objects hold the data (fields) and methods. When you create an object from a class, you supply the name of the object. We expose object behaviors through these methods. In OOP, objects are self-contained. The encapsulated code can include methods, variables, etc. When different methods are called, actually the function defined in the class is invoked. So, it is important to know which object calls the function. This type of construct allows you to write modular programming. It is useful for easy maintenance. Let us model a simple class, called Student . This class tells that it is a “ Student class ”, and it has one behavior which can say “ Hello ” to a student. Python helps you create the class using the following code: class Student: """ This is a simple Student class.""" name = "Student class" def say_hi(self,student_name): """ A simple method to say hello to a student.""" print(f"Hello {student_name}!") print(f"You are using {self.name} now.") There are some new things to learn, but don’t worry! You’ll be familiar with the structure shortly. I suggest that you continue reading the following information carefully. Notice that I use the class keyword to create a class. The class has a name, and the line ends with a colon ( : ). In the next line, you see a docstring which tells about the class. In real-world programming, docstrings are used to specify what a class can do in your code. POINTS TO REMEMBER I want you to note the following points carefully: The coding convention for a class name is: the first letter of each word is capitalized and you should not separate words using underscores. For example, MyClass, Student, ColorContainer, etc. are the standard class names. Instance names are lowercase case letters, and you can have underscores between the words. It’s a better practice to maintain a meaningful docstring in your class, and function(s). I recommend you to follow this practice. To type less, in the remaining chapter, I use some code fragments without docstrings. There I focus on some other aspects of a class. The indented code shows the class body. You can see this class has a variable, called name and a function, called say_hi(). Collectively, you can call them class members. Till now, you have the class definition only; it does not allocate any computer memory. You can see the ‘ self ’ parameter in the function definition. We use it to refer to the actual object. This parameter must appear before any other parameter(s). It is included in the method definition for a special purpose. When you call this method using an object of this class, this method automatically passes the self argument. You can use it to access the variable name that is defined inside the class. POINT TO REMEMBER The self parameter in a method definition comes before any other parameter(s) in the method definition. It has a special purpose. A Python method requires the object to be passed as the first argument to a class to distinguish the object from other objects of the class. It means that when you invoke a method using a class instance, the self argument is automatically passed. And it helps you to access individual attributes or methods of the class. (For a static method, ‘ self ’ is NOT required. You can skip that part for now). If you are familiar with Java or C#, you’ll find that self in Python is similar to this in Java or C#. Unlike C structures or Java classes, you can skip declaring the name variable in advance. Instead, you can create it on the fly in Python. I’ll discuss this shortly. Since I have created a class called Student, I can now create an instance (or, object) from it. Let us name the instance as object1 . I use the following code: #Creating an object from Student class object1=Student() The Student class has defined the method say_hi() which takes two arguments. I said before that self argument is passed automatically. So, when I invoke the method using this object, I pass only a student name as follows: #invoking the method object1.say_hi("John") If you execute this code fragment, you see the following output: Hello John! You are using Student class now. Let us create another object, called object2 . Then invoke the say_hi() method with a different name ( Kate ) as follows: #Creating another object from Student class object2=Student() #invoking the method object2.say_hi("Kate") This time you get the following output: Hello Kate! You are using Student class now. Hope you have got the idea! Now you are ready to execute your first object-oriented program. Let us go through the complete code in demonstration1. Demonstration 1 Create a new python file. Let us call it class_object_demo_1.py and type the following lines into it. # The class definition class Student: """ This is a simple Student class.""" name = "Student class" def say_hi(self,student_name): """ A simple method to say hello to a student.""" print(f"Hello {student_name}!") print(f"You are using the {self.name} now.") #Creating an object from Student class object1 = Student() #Invoking the method object1.say_hi("John") #Creating another object from Student class object2 = Student() #Invoking the method object2.say_hi("Kate") Output When you run this program, you will get the following output. Hello John! You are using the Student class now. Hello Kate! You are using the Student class now. Analysis If you have a Java or C# background, you may find some similarities. For example, I define an attribute(name) in the class before I use it in a print statement. But Python gives you more flexibility. You can define the attribute on the fly. You’ll see the example in demonstration3. POINTS TO REMEMBER A class is a logical entity. Once you instantiate a class, you create objects. These objects occupy memories in your system. So, the objects are physical entities. Constructor You use constructors to run initialization codes and make the objects. Constructors can be both parameterized and non-parameterized. When you use a parameterized constructor, you can pass a different number of arguments to them. In Python, there is a special method, called __init__(). It runs automatically whenever you create an instance from the class. Following the convention, this method has two leading and two trailing underscores. Here is an example of a non-parameterized constructor in a class. Ideally, ‘non-parameterized’ means that there is no parameter. But in Python, __init__(self) takes the argument self automatically, and you do not need to pass anything inside it. So, we often refer to this constructor as a nonparameterized constructor. def __init__(self): print("You do not need to supply any parameter.") Consider the following code now. Here you see a class with a nonparameterized constructor as follows: class Student: def __init__(self): print("You do not need to supply a parameter.") # Creating an object from Student class. # Using the non-parameterized constructor. object1 = Student() If you execute this code fragment, you can see the following output: You do not need to supply a parameter. Now, look into a constructor that accepts one argument ( name ) from you. def __init__(self, name): print("You need to supply a name.") Likewise, here is another constructor that needs two arguments ( name and roll_no ) from you. def __init__(self,name,roll_no): print("Supply a name and a roll number.") and so on. Let us consider a class with a constructor. In the following case, the Student class has a constructor that has one parameter. I define it as follows: class Student: def __init__(self, name): print(f"You've supplied the name:{name}") Whenever you create an object from this class, you need to supply a value for the name parameter. The following code with supportive comments shows you such a usage: # Creating an object from Student class. # Using the constructor that accepts one parameter. student_john = Student("John") If you execute this code, you get the following output: You've supplied the name:John Hope you understand the concept. Let us examine a complete program in demonstration2, and follow the discussion. POINTS TO REMEMBER Constructors are used for initialization. For now, to make these simple, I use some simple print statements in them. In the upcoming examples, you’ll see the true initialization statements inside these constructors. Demonstration 2 Create a new python file. Let us call it class_object_demo2.py and type the following lines into it. class Student: """ This is a Student class with a constructor.""" def __init__(self, name, roll_no): """ The name and roll_no is used to identify a student properly. """ print(f"The student name is: {name}") print(f"The student roll number is: {roll_no}") # Creating an object from Student class. # Using the constructor that accepts two parameters. student_robin = Student("Robin", 2) Output When you run this program, you see the following output. The student name is: Robin The student roll number is: 2 Analysis In demonstration 1, I created objects from the Student class, but you do did not see any constructor in that example. It was possible because if you do not supply any constructor for your class, Python calls the superclass constructor for you. To understand ‘Superclass’, you need to learn inheritance, which I discuss later in this chapter. In Python 3.x, the object class is the root of all classes. Once you learn inheritance, you’ll understand that class Student and class Student(object) are essentially the same. So, once you learn inheritance, you can read this paragraph again. In this demonstration, when __init__(…) was called, the argumentsRobin and 2 are assigned to the name and roll_no attribute of the Student class. You may ask me: what is the advantage? The answer is: you can make different objects where you set different values for these attributes. For example, the following line of code: student_kate = Student("Kate", 4) can create another instance of Student class. Using the constructor, you set name and roll_no attributes with the values- Kate and 4 . This time your instance name is student_kate . In other words, the variable student_kate holds the instance which has Kate as a student name and 4 as her roll number. Looking into Instance Creation Process In OOP, understanding the object creation process is important. I want you to understand it clearly. In demonstration 1, there is a name attribute but there is no constructor. In demonstration2, you see no attribute, but you notice a constructor. Now we’ll look into these in detail. Notice that in demonstration 2, when I create the instance, student_robin using the following line: student_robin = Student("Robin",2) Python calls the constructor _ _init__(self, name, roll_no) to create the object. You can see that I pass two arguments Robin and 2 during the object creation. I use the following code to print those values: print(f"The student name is:{name}") print(f"The student roll number is:{roll_no}") Now I make some minor changes in demonstration2 and rename it demonstration 3. It is almost a replica of demonstration2, but this time I use the following statements: print(f"The student name is:{self.name}") print(f"The student roll number is:{self.roll_no}") Have you noticed that this time you see the use of ‘ self ’? Fine, first go through the following demonstration. Then follow the discussion to understand it. Demonstration 3 Here is the complete demonstration. Create a new python file. Let us save it with a new name, say class_object_demo3.py, and type the following lines into it. class Student: """ This is a Student class with a constructor.""" def __init__(self, name, roll_no): """ The name and roll_no is used to identify a student properly. """ print("This constructor has two parameters.") #initialize name self.name = name #initialize roll_no self.roll_no = roll_no print(f"The student name is: {self.name}") print(f"The student roll number is: {self.roll_no}") # Creating an object from Student class. # Using the constructor that accepts two parameters. student_jack = Student("Jack", 3) Output When you run this program, you can expect to see the following output which is similar to demonstration2. This constructor has two parameters. The student name is: Jack The student roll number is:3 Analysis Now analyze the changes. Notice that the dot(.) notation helps you to access the attribute of a particular instance. This is why I use the following lines: print(f"The student name is:{self.name}") print(f"The student roll number is:{self.roll_no}") to access the name and roll_no attributes of an instance. This demonstration shows you another important characteristic. I talked about it earlier. Have you noticed that I didn’t declare name and roll_no in advance in the class? In Python programming, you can create data fields on the fly. This is why, when I use the following lines inside the constructor: self.name=name self.roll_no=roll_no there was no problem though I haven’t defined name and roll_no variables earlier. Changing an Attribute Value You can update the attribute value inside an object (or instance). For example, in demonstration 3, you create an instance, called student_jack as follows: student_jack = Student("Jack",3) Notice that inside student_jack , the attribute name has the value Jack and the attribute roll_no has the value 3 . Let us say, you want to assign different values in this instance. The easiest way is to directly access the attribute and change the value as follows: # Changing the attribute values of student_jack student_jack .name = "Bob" student_jack .roll_no = 5 One interesting point: student_jack is a better name than object1 or object2 , but it is not good when you replace the name Jack with Bob . My aim here is to show you how to change these values. I always suggest picking a meaningful name for your object. Alternatively, you can define a function inside your class to update an instance value. So, once you initialize the object, you can use the instance method to update the attribute values. In the upcoming demonstration, you’ll see both usages. Demonstration 4 In this demonstration, I update a student’s name and roll_no multiple times. Initially, I create a Student class object, called student_1 which has the name, Jack. I assign his roll_no to 3 . It is exactly similar to demonstration3. Next, I change the attribute values directly to Bob and 4 using the following lines: student_1.name="Bob" student_1.roll_no=5 Again, I update the values to Harry and 6 , but this time I use a method. I define it as follows: def update_student_details(self,new_name,new_roll_no): self.name=new_name self.roll_no=new_roll_no I invoke this method using the dot operator as follows: student_1.update_student_details("Harry",6) The remaining code is easy to understand. To test this, create a new python file and save it with a new name, say class_object_demo4.py. Then type the following lines into it. For your easy understanding, you can follow the associated comments. class Student: """ This is a Student class with a constructor.""" def __init__(self, name, roll_no): """ The name and roll_no is used to identify a student properly. """ #initialize name self.name = name #initialize roll_no self.roll_no = roll_no def get_details(self): """ This method prints the detail of a student. """ print("The current student's detail is:") print(f"Name: {self.name}") print(f"Roll number: {self.roll_no}") def update_student_details(self, new_name, new_roll_no): """ This method is used to update a student's detail. :param new_name: Updated student name. :param new_roll_no: Updated student roll_no :return: None """ self.name = new_name self.roll_no = new_roll_no # Creating an object from Student class. student_1 = Student("Jack", 3) #printing the details student_1.get_details() # Changing the attribute values of student_1 student_1.name = "Bob" student_1.roll_no = 5 print("---After the first update.---") student_1.get_details() #Second update using the method 'update_student_details' student_1.update_student_details("Harry", 6) print("---After the second update.---") student_1.get_details() Output You can get the following output when you execute this program. The current student's detail is: Name: Jack Roll number: 3 ---After the first update.--The current student's detail is: Name: Bob Roll number: 5 ---After the second update.--- The current student's detail is: Name: Harry Roll number: 6 Working with Default Attributes Till now you have seen the demonstrations where I pass the values for the attributes of an instance. But it is not mandatory. Consider our Student class again. So far, I was passing the name and roll_no of a student. But consider a case, where all students belong to the same college or university. Here, if you want your Student class object to having the college name, you do not need to pass the repeated information. Demonstration 5 The following example shows such a case. Here a student object automatically belongs to a college, called St.Stephen . This example is like the previous demonstration, but I have excluded the update method. Here my focus is on the default attribute value. I have accomplished it using the following line inside the constructor: self.college="St.Stephen" To include the college information, I have changed the get_details() method. Let us create a new python file and save it with a new name, say class_object_demo5.py. Then include the following lines to verify the result: class Student: def __init__(self,name,roll_no): #initialize name self.name = name #initialize roll_no self.roll_no = roll_no self.college = "St.Stephen" def get_details(self): print("The current student's detail is:") # Formatting the output using f-string # This syntax is available from Python 3.6 onwards) print(f"Name: {self.name}") print(f"Roll number: {self.roll_no}") print(f"College: {self.college}") # Creating an object from Student class. student_1 = Student("Jack", 3) #printing the details student_1.get_details() # Creating another object from Student class. student_1 = Student("Kate", 4) #printing the details student_1.get_details() Output Here is the output of the program. The current student's detail is: Name: Jack Roll number: 3 College: St.Stephen The current student's detail is: Name: Kate Roll number: 4 College: St.Stephen Analysis Notice that you do not pass the college name in the constructor. Still, you have an attribute to hold the default college name. Each student object can contain this information inside it. Importing Classes In a complex real-world application, the code size is gigantic. If you write the entire code into a single file, you create a huge file. This kind of practice is commonly discouraged. Instead, Python allows you to store segregate your code in modules. In Chapter 7, I told you that a module can contain many things such as variables, functions, and classes. In this chapter, our focus is on classes. So, here I show you how to import classes from a module. To show you a simple example, I introduce a class called Employee. It is like the Student class that we discussed so far. The only differences are: instead of a roll number and a college name, now you see an employee id and a company name. I choose the default company name as Abc Ltd. Here is the class that I store in a separate file called employee_module.py. I keep lots of comments for your easy understanding. I’ve organized all the codes chapter wise. For example, I store all programs of Chapter 11 inside a directory named chapter11 . The parent directory of the chapter11 is PythonCrashCourse . If I store the employee_module.py inside the chapter11 directory, every time I refer to this module, I need to mention it as chapter11.employee_module . So, to type less, I store the file employee_module.py in PythonCrashCourse . Now I can directly refer to the module without mentioning the chapter11. # This is an Employee class where you can # find the name, employee id, and company details. # It is used in chapter11. """ Class --------Employee This is a simple class. In the constructor, you can store employee name, employee id and company information. It has a method, called get_details() to print these information. """ class Employee: """ It is a simple class to store basic employee information. """ def __init__(self, name, emp_id): # Initialize name self.name =Iname # Initialize emp_id self.emp_id = emp_id self.company = "Abc Ltd." def get_details(self): print("\nThe current employee details are:") # Formatting the output using f-string print(f"Name: {self.name}") print(f"Id: {self.emp_id}") print(f"Company: {self.company}") Demonstration 6 Create a new python file. Save it with a new name, say importing_single_class.py, and type the following lines into it. from employee_module import Employee # Creating an object from the Employee class. emp_1 = Employee("Jack", 3) # Printing the details emp_1.get_details() # Creating another object from the Employee class. emp_2 = Employee("Kate", 4) # Printing the details emp_2.get_details() Output Once you run the program, you can see the following output. The current employee details are: Name: Jack Id: 3 Company: Abc Ltd. The current employee details are: Name: Kate Id: 4 Company: Abc Ltd. Analysis You can see that importing_single_class.py contains only a few lines of code. I can create the Employee class instances in the current file because I imported this class from employee_module . Demonstration 7 You have just seen a demonstration where you imported a class from a module. You can import multiple classes from a module, or you can import the entire module. To show you an example, I add the following class inside the file employee_module.py that I used earlier: class PersonalDetails: """ This class is used to show the background details of an employee. """ def __init__(self, name, current_company, work_experience, age, address): # Initialize department self.name = name self.current_company = current_company self.work_experience = work_experience # Initialize age self.age = age self.address = address def background_details(self): print("-" * 10) print("Here are the background details:") #Formating the output using f-string print(f"Name: {self.name}") print(f"Current company: {self.current_company}") print(f"Age: {self.age},Address: {self.address}") print(f"Experience: {self.work_experience} yrs.") print("-" * 10) Create a new python file. Save it with a new name, say importing_multiple_classes.py. Then type the following lines into it. from employee_module import Employee, PersonalDetails # Creating an object from Employee class. emp_1 = Employee("Jack", 3) # Setting background details for emp_1 emp_1_details = PersonalDetails(emp_1.name, emp_1.company, 10.2, 39, "21,Abc Road,USA") # Printing the details emp_1.get_details() emp_1_details.background_details() Output Run the program. You can see the following output. The current employee details are: Name: Jack Id: 3 Company: Abc Ltd. ---------Here are the background details: Name: Jack Current company: Abc Ltd. Age: 39,Address: 21,Abc Road,USA Experience: 10.2 yrs. ---------- It is a recommended practice to make a module with related classes. In demonstration7, you have seen that employee_module contains Employee class and PersonalDetails class. These two are related classes. Demonstration 8 When you import an entire module, you can access its classes using the dot notation. To illustrate, consider the following example. It is an alternative version of the previous demonstration. import employee_module # Creating an object from the Employee class. emp_1 = employee_module.Employee("Jack", 3) # Setting background details for emp_1 emp_1_details= employee_module.PersonalDetails( emp_1.name, emp_1.company, 10.2, 39, "21,Abc Road,USA") # Printing the details emp_1.get_details() emp_1_details.background_details() When you run the program, you’ll see the same output which you saw in demonstration7. Demonstration 9 Now I show another way of importing classes from a module. It is not a recommended practice, but I present this to you for the sake of completeness purpose. You get the same output as in demonstration8(or demonstration7) if you change the first line in the previous demonstration as follows: from employee_module import * # NOT a recommended practice #remaining code as it is It is not a recommended practice due to the following reasons: You cannot directly see which classes are imported in the current file. We do not need all the imported classes in the current program file. If the program file has a same-named class, there will be a name collision. To illustrate the third bullet point, let us add another Employee class in the program file. from employee_module import * # NOT a recommended practice class Employee: def __init__(self): print("Inside the constructor of Employee class") # Creating an object from the Employee class. emp_1 = Employee("Jack", 3) ##Name collision # Setting background details for emp_1 emp_1_details = PersonalDetails( emp_1.name, emp_1.company, 10.2, 39, "21,Abc Road,USA") # Printing the details emp_1.get_details() emp_1_details.background_details() If you run this program, you can get an error like the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter11/importing_all_classes_from_a_m line 10, in <module> emp_1 = Employee("Jack",3) TypeError: __init__() takes 1 positional argument but 3 were given So, what is the remedy? In this case, you can import the entire module and use the dot notation to point to the correct class. Here is a sample implementation. import employee_module #from employee_module import * # NOT a recommended practice class Employee: """ This employee class stays in the current file.""" def __init__(self): print("Inside the constructor of Employee class") # Creating an object from the Employee class. #emp_1 = Employee("Jack", 3) #Name collision emp_1 = employee_module.Employee("Jack", 3) # Setting background details for emp_1 #emp_1_details = PersonalDetails( emp_1_details = employee_module.PersonalDetails( emp_1.name, emp_1.company, 10.2, 39, "21,Abc Road,USA") # Printing the details emp_1.get_details() emp_1_details.background_details() emp_2 = Employee() # Uses current file’s Employee class Output Run the program. You can see the following output. The current employee details are: Name: Jack Id: 3 Company: Abc Ltd. ---------Here are the background details: Name: Jack Current company: Abc Ltd. Age:39,Address:21,Abc Road,USA Experience:10.2 yrs. ---------Inside the constructor of the Employee class The last line shows that you can use the Employee class that stays in the current file. Use of Identity Operator In Chapter 2, I told you I discuss the identity operators in this chapter. Now you know about objects. So, you are ready for it. The identity operator can tell whether two comparing objects are the same object with the same memory location. In the following demonstration, you see three objects. I call them jack, jack_2, and jack_3 respectively. All these objects have the same content. But you’ll see that I create jack_2 separately, but I create jack_3 from an existing object, jack. So, jack_3 and jack are the same, but jack and jack_2 are not the same objects. You can use the is and is not operators to check this exactness. The ‘ is’ operator returns True if both variables point to the same object. The ‘ is not’ can check the reverse. Demonstration 10 Here is the complete demonstration. class Student: """ This is a Student class with a constructor.""" def __init__(self, name, roll_no): """ The name and roll_no is used to identify a student properly. """ self.name = name #initialize roll_no self.roll_no = roll_no print(f"Name: {self.name}") print(f"Roll number is: {self.roll_no}") # Creating an object from Student class. # Using the constructor that accepts two parameters. print("Creating jack object:") jack = Student("Jack", 3) print("\nCreating jack_2 object:") jack_2 = Student("Jack", 3) print("\nUse of identity operator:") print("jack is jack_2?", jack is jack_2) # False print("Introducing jack_3 object") jack_3 = jack print("jack_3 is jack?", jack_3 is jack) # True print("jack_3 is jack_2?", jack_3 is jack_2) # False Output Here is the output. Creating jack object: Name: Jack Roll number is: 3 Creating jack_2 object: Name: Jack Roll number is: 3 Use of identity operator: jack is jack_2? False Introducing jack_3 object jack_3 is jack? True jack_3 is jack_2? False You can use these operators to check if the variable is of a certain type. Here is a short code segment for you: >>> x=10 >>> type(x) is int True >>> type(x) is str False >>> type(x) is float False >>> type(x) is not float True So, in demonstration10, if you append the following lines: print("The type(jack) is Student?") print(type(jack) is Student) # True print("The type(jack) is int?") print(type(jack) is int) #False print("The type(jack) is NOT int?") print(type(jack) is not int) # True You see some more output for these new lines of code. These are as follows: The type(jack) is Student? True The type(jack) is int? False The type(jack) is NOT int? True Inheritance The primary aim of inheritance is to promote reusability and eliminate redundancy in code. It also shows how a child class can get the features (or characteristics) of its parent class. A parent class is placed at a higher level in the class hierarchy, and a child class can derive from it. So, we also refer to a child class is as a derived class or subclass. We often refer to a parent class as a superclass. In short, when you make a specialized version of a parent class, you can avoid writing the code from scratch. Instead, you use inheritance to get the attributes and methods of the parent class. A child class can use any number of attributes or methods of the parent class. It can also define new attributes and methods of its own. Let us start with a simple example. In the following example, Shape is the parent class. You can make any class a parent class. So, the syntax of creating a parent class is the same as creating any other class. Here is the Shape class for you. You can see that it has a constructor, and a method called about_me() . class Shape: """ This is the parent class """ def __init__(self): self.type = "Shape" def about_me(self): print(f"I am a {self.type}.") To create a child class, you use the parent class as a parameter. In our upcoming example, the Rectangle class inherits from the Shape class. So, you notice the following line: class Rectangle(Shape): #Remaining code In the following example, I use one Shape object and one Rectangle object. The Shape object( shape_ob ) can access the about_me() method. It is obvious. But notice that the Rectangle object( rectangle_ob ) can also access the method. It is possible because the Rectangle class inherits from the Shape class. Demonstration 11 Here is the complete demonstration. I have created a new Python file, named inheritance_ex1.py, and place the following code inside it. class Shape: """ It is the Shape class. It is the parent class of Rectangle in this example. """ def __init__(self): self.type = "Shape" def about_me(self): print(f"I am a {self.type}.") class Rectangle(Shape): """ This is the Rectangle class which inherits from the Shape class """ print("***Simple inheritance example.***") shape_ob = Shape() shape_ob.about_me() rectangle_ob = Rectangle() rectangle_ob.about_me() Output Here is the output. ***Simple inheritance example.*** I am a Shape. I am a Shape. Analysis You can see that the Rectangle class object can access the parent class method. I show this for a demonstration purpose. We know that you can have a different type of shape, such as a rectangle, a circle, or a square. So, we can override the parent class method inside the Rectangle class. It is better to use the child class constructor to tell that it is a Rectangle type. Let us update demonstration10 now. Updated Implementation Here is an updated implementation of Demonstration10. class Shape: """ This is the parent class """ def __init__(self): self.type = "Shape" def about_me(self): print(f"I am a {self.type}.") class Rectangle(Shape): """ This is the Rectangle class which inherits from the Shape class """ def __init__(self): self.type = "Rectangle" def about_me(self): print(f"I am a {self.type}.") print("***Simple inheritance example.***") shape_ob = Shape() print(f"The shape_ob.type={ shape_ob.type}") shape_ob.about_me() rectangle_ob = Rectangle() print(f"The rectangle_ob.type={ rectangle_ob.type}") rectangle_ob.about_me() Output Here is the output of this implementation. ***Simple inheritance example.*** The shape_ob.type=Shape I am a Shape. The rectangle_ob.type=Rectangle I am a Rectangle. This updated implementation shows you the following characteristics: You can modify the attribute of the parent class inside the child class. If needed, you can override a parent class method. When I say “Overriding” a parent class method, notice that the method names in the parent class and child class are the same. You may often see a class with many attributes and methods. In a specialized version of this class, you do not change all of them. Instead, you modify a few of them. It is also possible that you add a few attributes or methods in the specialized (or child ) class. The upcoming demonstration shows you a similar usage. In demonstration6, you saw an Employee class as follows: class Employee: """Employee class is the parent class.""" def __init__(self, name, emp_id): # Initialize name self.name = name # Initialize emp_id self.emp_id = emp_id self.company = "Abc Ltd." Make an inheritance hierarchy by adding a Developer class now. A Developer is a special type of Employee . So, we do not need to change the basic employee information, such as employee name, company name, employee id. You can inherit them from the Employee class. Later you can add a designation attribute in the Developer class. You can achieve this when you call the __init()__ method of the parent class using the super() function. The super() function is a special function that allows you to call a parent class method. The name comes from a convention of calling a parent class a superclass and a child class a subclass. This is why you’ll see the following code in the upcoming demonstration: class Developer(Employee): """Developer class inherits from Employee""" def __init__(self, name, emp_id): """ Initialize starts from parent class.""" super().__init__(name, emp_id) # This is a class-specific attribute self.designation = "Developer" Now you can add a method to describe a developer detail. I present the following demonstration to show you the usage. Demonstration 12 Here is the complete demonstration. I have created a new Python file, named inheritance_ex2.py, and place the following code inside it. Here is the complete demonstration. class Employee: """Employee class is the parent class.""" def __init__(self, name, emp_id): # Initialize name self.name = name # Initialize emp_id self.emp_id = emp_id self.company = "Abc Ltd." class Developer(Employee): """Developer class inherits from Employee""" def __init__(self, name, emp_id): """ Initialize starts from parent class.""" super().__init__(name, emp_id) # This is a class specific attribute self.designation = "Developer" def developer_details(self): """Prints the developer details.""" print(f"Name: {self.name}") print(f"Id= {self.emp_id}") print(f"Designation= {self.designation}") print("-" * 10) # Creating an instance of the Developer class john = Developer("John S", "E001") john.developer_details() kate = Developer("Kate W", "E002") kate.developer_details() Output Here is the output. Name: John S Id= E001 Designation= Developer ---------Name: Kate W Id= E002 Designation= Developer ---------POINT TO REMEMBER In demonstration11, if you replace the following line: super().__init__(name, emp_id) with the following line: Employee.__init__(self, name, emp_id) #Also works You get the same output. The difference is that when you use super() , you do not pass the parent class name and the self parameter. But this technique is not suitable in the long run. For example, consider a case, when you need to change the design or inheritance hierarchy. Since you hard-code the inherited class name, you need to refactor your code. This is the reason an expert programmer normally prefers to use super() in his code. This approach promotes indirection because we do not need to mention the delegate class by name. Private Variables and Methods If you are familiar with object-orient programming languages like Java, C#, C++, etc., you notice the use of public, private, and protected variables (and methods). If not, let me give you the idea with simple examples. Consider your ATM card or Credit card credentials. You do not share this information with anybody else. These are your private data. In my case, my kids are among the trusted ones who know where I keep my ATM cards at my home. You can call it the protected data. In programming languages like Java, apart from the containing class, the child classes can access the protected variables (and methods). In simple words, you extend the visibility to some extent, but not for everyone. Finally, anyone can access public information. So, if I place a noticeboard outside my house to say when I like to meet my visitors - this is a piece of public information. Similarly, the advertisement for recruitment or visiting hours of a hospital represents the public data. In programming, public data has maximum visibility. In Java, C#, or C++, declaring a variable public, private, or protected is straightforward. For example, you use the keyword “private” to declare a private variable. Now you may want to know whether Python supports a similar concept. The simple answer is: Python does not have real private variables. In Python programming, you can use the underscore(s) in a similar context. You can use two underscores at the beginning of a variable (or a method) to mark it as a private variable (or a private method). It tells that you cannot access the variable (or the method) in a usual way. Let us test this with an example. Demonstration 13 Consider the following class. class Parent: """ This is the parent class""" def __init__(self): self.i = 1 #public variable self.__j = 10 #private variable def __private_show_me(self): print(f"i={self.i}") print(f"__j={self.j}") def about_me(self): print(f"i={self.i}") print(f"__j={self.__j}") Before you proceed further, make an object of Parent class as follows: parent_ob = Parent() In the Parent class, i is not a private variable. You can use it directly as follows: print(f"parent_ob.i={parent_ob.i}") Which can output the following: parent_ob.i=1 But __j is the private variable. So, if you try to access it directly as follows: print(parent_ob.__j)#will cause error You get the error like the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter11/private_variable_and_method.py" line 19, in <module> print(parent_ob.__j)#will cause error AttributeError: 'Parent' object has no attribute '__j' Now see the methods. The about_me() method is not private. So, you can access this method directly. Since this method stays in the Parent class, it can access the private variable too. So, the following code can work for you: parent_ob.about_me() It can produce the following output: i=1 __j=10 Finally, ___private_show_me() is a private method. So, if you try to access it in the following way: parent_ob.__private_show_me() #will cause error You see the error again, which is like the following: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter11/private_variable_and_method.py" line 23, in <module> parent_ob.__private_show_me() #will cause error AttributeError: 'Parent' object has no attribute '__private_show_me' Hope you have got the idea! Now I present you the complete demonstration with supportive comments. If you uncomment the corresponding codes, you will get errors. I have kept them for your reference purpose. class Parent: """ This is the parent class""" def __init__(self): self.i = 1 #public variable self.__j = 10 #private variable def __private_show_me(self): print(f"i={self.i}") print(f"__j={self.j}") def about_me(self): print(f"i={self.i}") print(f"__j={self.__j}") parent_ob = Parent() print(f"parent_ob.i={parent_ob.i}") #print(parent_ob.__j)#will cause error print("Calling about_me() now:") parent_ob.about_me() #parent_ob.__private_show_me() #will cause error Output Here is the output. parent_ob.i=1 Calling about_me() now: i=1 __j=10 Analysis Is there any way to access the private variables? Yes, you have. I’ve shown you one way in the previous demonstration when a public method could access the private data. Is there any alternative way? Yes, the following code can work for you: print(parent_ob._Parent__j)#it works it can print the output: 10 Notice this again. You can see that _parent_ob._Parent__j represents the format <instance_name>._Classname__privatevariable Python saves the private variable in this different format. As a result, you cannot use this variable outside of the class in the usual way. We call this name mangling. This is useful to avoid the ambiguity of names defined by a subclass. In other words, if you do not want a subclass to use its parent class attributes, you can name those attributes with double leading underscores and no trailing underscore. Actually, in this context, Python allows you to use at most one trailing underscore, which means you can use a variable like __z or __z_. Static Methods and Class Methods Till now, you have seen the usage of instance methods. In OOP, they are very common. Now I show you static methods and Python-specific class methods. These are useful when you learn advanced programming in Python. It may seem complicated at the beginning, but a careful analysis can make things clear to you. Let us begin with a class with a constructor and an instance method. You are familiar with this: class Color: """ This is the parent class""" fav_color = "Green" def __init__(self, color): self.fav_color = color def instance_method(self): print("I am an instance method.") print("Call me with an instance and a dot operator.") print(f"My favorite color is: {self.fav_color}") print("+"*15) You understand that I can create an object of Color class and call the instance_method . Here is a sample code segment: #Creating an object from Student class my_color = Color("Blue") print("Calling the instance_method() now.") my_color.instance_method() If you run this code, you can get the following output: Calling the instance_method() now. I am an instance method. Call me with an instance and a dot operator. My favorite color is: Blue +++++++++++++++ Till this point, everything is straightforward. Now I introduce a static method inside the Color class. You can see the static methods in other languages too, such as Java, C#. Read the following points for static methods in Python: You use a decorator @staticmethod before the method definition. This time you do not use self parameter. Instead, you use the class name Color to invoke the variable fav_color . @staticmethod def static_method(): print("I am a static method.") print("You can call me without a class instance.") print(f"My favorite color is: {Color.fav_color}") print("+" * 15) The following code segment can invoke the static method as follows: Color.static_method() and produce the following output: I am a static method. You can call me without a class instance. My favorite color is: Green +++++++++++++++ Now I introduce a class method inside the Color class. It’s a special inclusion in Python programming. Notice the following points in this segment: You use a decorator @classmethod before the method definition. This time you do not use self parameter. Instead, you use cls with a dot operator to invoke the variable fav_color . It is similar when you invoke an instance method. The only difference is that instead of using the instance name, you use cls . I choose the name ‘cls’ following the convention. So, if you use any other name, that will work too, but experts do not recommend that. @classmethod def class_method(cls): print("I am a class method.") print("You can call me without a class instance.") print("In general, you use cls as the class parameter.") print(f"My favorite color is: {cls.fav_color}") print("+" * 15) The following code segment can invoke the class method as follows: Color.class_method() and produce the following output: I am a class method. You can call me without a class instance. In general, you use cls as the class parameter. My favorite color is: Green +++++++++++++++ Understanding the Difference Let me change the color now. You can append the following code and run the program again. print("Changing the color inside the instance now.") my_color.fav_color = "Red" print("Calling the instance_method() now.") my_color.instance_method() print("Calling the static_method() now.") Color.static_method() print("Calling the class_method() now.") Color.class_method() This time you can see these extra outputs: Changing the color inside the instance now. Calling the instance_method() now. I am an instance method. Call me with an instance and a dot operator. My favorite color is: Red +++++++++++++++ Calling the static_method() now. I am a static method. You can call me without a class instance. My favorite color is: Green +++++++++++++++ Calling the class_method() now. I am a class method. You can call me without a class instance. In general, you use cls as the class parameter. My favorite color is: Green +++++++++++++++ Notice that the color “ Red ” is reflected when you use the instance method. But the static method or the class method did not change the color. It is because both the class method and the static method are bound to a class, but not to an object. Now we do some further analysis. See the code examples of instance methods, static methods, and class methods again. Can you remember the functions in Chapter 7 ? If you compare these codes with those functions, you understand that my_color.instance_method() transforms the code to instance_method(my_color). You called the static method and the class method using class names, which are as follows: Color.static_method() Color.class_method() It is interesting to note that you can call them in an alternative way using the instance name. So, the following code will work too: #Also works my_color.static_method() my_color.class_method() You understand that behind the scene, my_color.static_method() actually transforms to static_method(). It means no additional argument is added in the function call. But my_color.class_method() call transforms to class_method(type(cls)) . This is helpful when you use inheritance. It helps you to know about-which class calls this method. To understand this go through the following demonstration and output. Demonstration 14 In this example, there are two classes- Parent and Child . As per their names, the Parent is a superclass and the Child is a derived class. The Parent class contains an instance method, a static method, and a class method. I use both a Parent class object and a Child class object to access the methods. In the output, you can see which type of object calls the class method. For example, you can notice the following lines in this output: A class method is called.<class '__main__.Parent'> A class method is called.<class '__main__.Child'> Here is the complete demonstration: class Parent: """ This is a Parent class.""" def instance_method(self): print(f"An instance method is called.{self}") @staticmethod def static_method(): print("A static method is called.") @classmethod def class_method(cls): print(f"A class method is called.{cls}") class Child(Parent): """ This is a Child class.""" pass #Creating an object from Parent class parent_ob = Parent() print("Using the Parent class object.") parent_ob.instance_method() Parent.static_method() parent_ob.static_method() # Also ok Parent.class_method() parent_ob.class_method() # Also ok print("*"*20) print("Using the Child class object.") child_ob = Child() child_ob.instance_method() Child.static_method() child_ob.static_method()# Also ok Child.class_method() child_ob.class_method()# Also ok Output Here is the output. Using the Parent class object. An instance method is called.<__main__.Parent object at 0x0170C058> A static method is called. A static method is called. A class method is called.<class '__main__.Parent'> A class method is called.<class '__main__.Parent'> ******************** Using the Child class object. An instance method is called.<__main__.Child object at 0x0170C070> A static method is called. A static method is called. A class method is called.<class '__main__.Child'> A class method is called.<class '__main__.Child'> Analysis From this output, you can tell whether the Parent class object calls the class method or the derived class object calls it. You’ll find this mechanism useful when you make an advanced application. But static methods do not tell any such information. It is an important difference between a class method and a static method. EXERCISE E11.1 Can you predict the output of the following code segment? class Student: class_name = "Student" def __init__(self, roll_number): self.roll_number = roll_number def about_me(self,name): """ A simple method """ print(f"Hello,{name}!") print(f"You belong to {self.class_name} class!") print(f"Your roll number is:{self.roll_number}") student_1 = Student(25) student_1.about_me("Mike") E11.2 Suppose you replace the following line in E11.1: student_1 = Student(25) with student_1 = Student() Can you predict the output? E11.3 Can you predict the output of the following code segment? class College: college = "Abc" def __init__(self,name,stud_name,roll_number): self.college = name self.stud_name = stud_name self.roll_number = roll_number def about_me(self): print(f"College name is: {self.college}") print(f"The student name is: {self.stud_name}") print(f"The roll number is: {self.roll_number}") student_1 = College("St. Stephen","Bob",2) student_1.about_me() E11.4 Can you predict the output of the following code segment? class Student: def __init__(self, roll_number, name="Mike"): self.name = name self.roll_number = roll_number def about_me(self): print(f"Name is: {self.name}") print(f"The roll number is: {self.roll_number}") student_1 = Student("Bob",1) print("\nStudent-1 detail:") student_1.about_me() print("\nStudent-2 detail:") student_2 = Student(2) student_2.about_me() E11.5 Can you predict the output of the following code segment? class Parent: @staticmethod def static_method(): print("Static method inside Parent is called.") @classmethod def class_method(cls): print(f"Class method is called.Invoker type:{cls}") class Child(Parent): """ This is a Child class.""" @staticmethod def static_method(): print("The static method inside Child is called.") parent_ob = Parent() child_ob = Child() parent_ob.class_method() child_ob.class_method() parent_ob.static_method() child_ob.static_method() E11.6 Can you predict the output of the following code segment? class ParentClass: @staticmethod def static_method(): print("Static method inside Parent is called.") class ChildClass(Parent): pass ParentClass.static_method() ChildClass.static_method() E11.7 Can you predict the output of the following code segment? ParentClass.static_method() ChildClass.static_method() class SampleClass: """ This is the parent class""" def __init__(self): self.i = 1 self.__j = 10 def about_me(self): print(f"i={self.i}") print(f"__j={self.__j}") sample = SampleClass() sample.about_me() print(sample.i) E11.8 Suppose, in E11.7, append the following line: print(sample.__j) Can you predict the output? E11.9 You have seen the differences between a class method and a static method. Can you tell some similarities between them? Solution to Exercises E11.1 Hello,Mike! You belong to Student class! Your roll number is:25 E11.2 In this case, you’ll receive the error like: Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter11/chap11_exercise.py", line 19, in <module> student_1 = Student() #error. Ex-11.2 TypeError: __init__() missing 1 required positional argument: 'roll_number' E11.3 In this case, you have updated the college name in the constructor, you see the updated college name as St. Stephen. Now you receive the following output. College name is: St. Stephen The student name is: Bob The roll number is: 2 E11.4 In chapter7, you learned about default values. This code segment uses the same concept. So, when you do not pass the student name, it accepts the default name as Mike. But notice that you need to supply the roll number, which you cannot bypass. Student-1 detail: Name is: 1 The roll number is: Bob Student-2 detail: Name is: Mike The roll number is: 2 E11.5 You can refer to the discussion about static methods and class methods in this chapter to understand the output. Class method is called.Invoker type:<class '__main__.Parent'> Class method is called.Invoker type:<class '__main__.Child'> Static method inside Parent is called. The static method inside Child is called. E11.6 Commonly, you invoke a static method (or a class method) using the class name. You can refer to the discussion about static methods in this chapter to understand the output. Static method inside Parent is called. Static method inside Parent is called. E11.7 i=1 __j=10 1 E11.8 This time you see an error.It is because you try to access a private data using an object in an illegal way. If needed, refer the discussions on private variables in this chapter.Here is the output. i=1 __j=10 1 Traceback (most recent call last): File "E:/MyPrograms/Python/PythonCrashCourse/chapter11/chapter11_exercise_file_2.py", line 19, in <module> print(sample.__j) #Error #Ex-11.8 AttributeError: 'SampleClass' object has no attribute '__j' E11.9 Here are the similarities: Both the class method and the static method are bound to a class, but not to an object. In general, we use class names to invoke a class method or a static method. Remember that you cannot use a class name to invoke an instance method. CS 8.1 Implementation In this implementation, Engineering is the parent class. The ComputerScience class derives from this class. So, you see the following code: class ComputerScience(Engineering): #remaining code skipped Mathematics and Soft-skills are common subjects across all institutions. So, I initialize these two subjects in the Engineering class constructor (i.e., in its __init()__() method). The default institution name and special subject (the third subject) is used in the derived class constructor. You call the superclass constructor to get the common subject’s name. So, you see this: def __init__(self, college_name="St. Stephen", special_subject="Compiler design."): """ Initialize starts from parent class.""" super().__init__(college_name) self.subject_3 = special_subject The remaining codes follow the specification of this project/case study. Here is the complete demonstration for you: class Engineering: """Engineering is the parent class.""" def __init__(self, college_name): # Initialize name self.college = college_name self.subject_1 = "Mathematics." self.subject_2 = "Soft-skills." class ComputerScience(Engineering): """ ComputerScience class inherits from Engineering. Default institution is St.Stephen. """ def __init__(self, college_name=" St. Stephen", special_subject="Compiler design."): """ Initialize starts from parent class.""" super().__init__(college_name) self.subject_3 = special_subject def course_details(self): """Prints the course details of an institution.""" print(f"Institution name:{self.college} college.") print("Computer science course includes:") print(f"1:{self.subject_1}") print(f"2:{self.subject_2}") print(f"3:{self.subject_3}") print("-" * 10) # Computer science course at Presidency College cs_course1 = ComputerScience(" Presidency", "Python Programming") cs_course1.course_details() # Computer science course at St.Stephen College cs_course2 = ComputerScience() cs_course2.course_details() Possible improvements To reduce the code size, I skipped the exception handling mechanism and user inputs in this project implementation. You can consider them to make a better implementation. I leave this improvement exercise to you. Chapter 12: Test Your Code Testing software is always important. It gives us confidence. The presence of test teams is not limited to software organizations. You see them everywhere. These teams test a product in various ways and generate the reports. It’s possible, there are different modules in software and you work only in a particular module. So, even if you write the correct code, your job is not over. The test teams can integrate your code with other’s code to verify the software. But every time you cannot wait for the entire test report to come. You first want to know whether your code works fine. So, you can write some small tests. You term them as unit tests. These small tests can check whether a class or a function shows the expected behavior. This chapter gives you a quick overview of this topic. I repeat: there are different types of tests. The detailed coverage of them is beyond the scope of this chapter. This chapter shows you the tests which help you examine the functions and classes in your code. Here you’ll also learn how to run many tests together. The Code for Testing You write tests to verify some code. So, at first, you need to have the code. (It is possible that you write test cases in advance when the development code is not ready. I ignore this possibility here). My primary goals are to show you: How to write a test for a function. How to run this test. How to run multiple tests together. I start with a module that has two functions inside it. I name the module my_library_functions.py . I store it inside the main directory, PythonCrashCourse . The code inside the module is simple. The supporting documentations tell you : The make_total_double() accepts two numbers and returns double of their sum. The make_avarage() function takes three numbers and returns the average of them. In this function, the first two are compulsory arguments. But the final one is an optional argument. To make these functions simple and easy, I exclude the validation part. Here are the contents of the module my_library_functions.py # I write test cases to verify the following functions. """ Support to use custom functions. Functions --------make_total_double() This function takes two numbers and returns the double. make_total_double() This function takes three numbers and returns the average. The third number is optional. """ def make_total_double(first,second): """ This function takes two numbers and returns the double. """ total = first + second return total * 2 def make_average(first,second,third=0): """ This function takes three numbers and returns the average. The third number is optional. """ total = first + second + third return total / 3 POINT TO REMEMBER In chapter7, I told you I’ve organized the codes for this book chapter wise. So, I store all programs for chapter12 inside a directory named chapter12. The parent directory of the chapter12 is PythonCrashCourse. Suppose I store the file my_library_functions.py inside the chapter12 directory. In that case, I cannot use the following line: from my_library_functions import make_total_double Instead, I need to use it as follows: from chapter12.my_library_functions import make_total_double So, to avoid less typing, I follow this structure. This helps me to refer to the module without mentioning the chapter12. Testing Functions But before you write your test code, import the unittest module. To test a function from the specified location, you need to import the function too. So, you see the following lines at the beginning: import unittest from my_library_functions import make_total_double Then you create a class with a meaningful name. Its better to include the word Test in this context. This class should inherit from unittest.TestCase . So, you see the following line in the upcoming code: class TestEmployee(unittest.TestCase): Now you are ready to write your test. It’s better to choose a name which starts with ‘test’. Inside the test_make_total_double () function, you see the use of assert methods. In testing, assertions are very common. You can use various assertion methods. These methods help you test the actual value against the expected value. Table 12-1 shows you four of them. These are enough for you to understand the content of this chapter. Table 12-1 The assert methods that you can use in Chapter12 For example, in the following tests, I pass two numbers- 25 and 75.3 . If you add them and double this sum, you should get( 25+75.3)*2=200.6 . So, I write the following code: self.assertEqual(200.6, resultant_value) Now if the function returns any other value, the test containing this statement will fail. You can use an optional message inside this method. This message can help you when the test fails. For example, if you write the following: self.assertEqual(200.6+1, resultant_value, "The actual value and expected value does not match") #test fails The test will fail and display this message. I pick a sample output fragment for your reference. Notice the bold lines in this segment: Launching unittests with arguments python -m unittest E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_single_test.py in E:\MyPrograms\Python\PythonCrashCourse\chapter12 FAILED (failures=1) The actual value and expected value does not match 200.6 != 201.6 Expected :201.6 Actual :200.6 <Click to see difference> Traceback (most recent call last): File "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.1\plugins\python-ce\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals old(self, first, second, msg) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 912, in assertEqual assertion_func(first, second, msg=msg) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 905, in _baseAssertEqual raise self.failureException(msg) AssertionError: 201.6 != 200.6 : The actual value and expected value does not match … One last point for you: You’ll see the following if block in this example: if __name__ == '__main__': unittest.main() The special variable __name__ is set when the program executes. When you run this file as the main program, the __name__ is set to __main__ . Here, you call unittest.main() to run the test case. A test framework can work differently. If a test framework imports this file, the __name__ may not set to __main__ . In that case, it will not execute this block. Demonstration 1 I have created the file run_single_test.py which has a single test. Here is the content: import unittest from my_library_functions import make_total_double class LibraryFunctionsTestCases(unittest.TestCase): """ Tests for my_library_functions """ def test_make_total_double(self): """ Can we make the total double for the numbers 25,75.3 """ resultant_value = make_total_double(25, 75.3) self.assertEqual(200.6, resultant_value) if __name__ == '__main__': unittest.main() Output Here is a sample output: Launching unittests with arguments python -m unittest E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_single_test.py in E:\MyPrograms\Python\PythonCrashCourse\chapter12 Ran 1 test in 0.023s OK Process finished with exit code 0 Demonstration 2 This demonstration shows you can execute multiple tests together. In realworld programming, this is a common practice. I have created a new file run_multiple_tests.py which has three tests. Now I import everything from my_library_functions.py . So, I can access both the methodsmake_total_double() and make_average() in my current program file. Here is the content of this file: import unittest # importing everything from the module import my_library_functions as ml class LibraryFunctionsTestCases(unittest.TestCase): """ Tests for my_library_functions """ def test_make_total_double(self): """ Can we make the average of the numbers 25,75.3 """ resultant_value = ml.make_total_double(25, 75.2) self.assertEqual(200.4, resultant_value) #test passes def test_make_average_two_numbers(self): """ Can we make the the average of the numbers 25,75.8,and 0(optional)?""" resultant_value = ml.make_average(25, 75.8) self.assertEqual(33.6, resultant_value) # test passes #self.assertEqual(40, resultant_value) # test fails def test_make_average_three_numbers(self): """ Can we make the average of the numbers 10,20.9 and 30 ?""" resultant_value = ml.make_average(10, 20.9, 30) self.assertEqual(20.3,resultant_value) if __name__ == '__main__': unittest.main() I remind you that you may not see the correct indentation on your device. So, if required, you can refer to the actual code. It is available online at https://github.com/Vaskaran/PythonBookcamp.I told you about this link at the beginning of this book. You can download the complete code from there. The same comment applies for all code in this book. Output Here is the sample output from my computer: Testing started at 10:15 ... E:\MyPrograms\Python\PythonCrashCourse\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.1\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_multiple_tests.py Ran 3 tests in 0.003s OK Launching unittests with arguments python -m unittest E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_multiple_tests.py in E:\MyPrograms\Python\PythonCrashCourse\chapter12 Process finished with exit code 0 Analysis Let us deliberately fail one test case and check the output. It can depict a common scenario when you run multiple tests together. To make the test fail, I replace the following line: self.assertEqual(33.6,resultant_value) # test passes with the following one: self.assertEqual(40,resultant_value) # test fails And execute the tests again. You can see the following output. Once again, I highlight the important lines in bold. Testing started at 10:17 ... E:\MyPrograms\Python\PythonCrashCourse\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.1\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_multiple_tests.py Launching unittests with arguments python -m unittest E:/MyPrograms/Python/PythonCrashCourse/chapter12/run_multiple_tests.py in E:\MyPrograms\Python\PythonCrashCourse\chapter12 33.6 != 40 Expected :40 Actual :33.6 <Click to see difference> Traceback (most recent call last): File "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.1\plugins\python-ce\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals old(self, first, second, msg) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 912, in assertEqual assertion_func(first, second, msg=msg) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 905, in _baseAssertEqual raise self.failureException(msg) AssertionError: 40 != 33.6 During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 60, in testPartExecutor yield File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 676, in run self._callTestMethod(testMethod) File "C:\Users\Vaskaran Sarcar\AppData\Local\Programs\Python\Python38-32\lib\unittest\case.py", line 633, in _callTestMethod method() File "E:\MyPrograms\Python\PythonCrashCourse\chapter12\run_multiple_tests.py", line 19, in test_make_average_two_numbers self.assertEqual(40, resultant_value) # test fails Ran 3 tests in 0.015s FAILED (failures=1) Process finished with exit code 1 Assertion failed Assertion failed Assertion failed Testing Class The first two demonstrations in this chapter show you how to write tests for functions. Now you see tests for class. This time, I do not create a new class. Instead, I reuse the employee_module , which you saw in Chapter 11. This module has two classes- Employee and PersonalDetails . Here I write a test to verify the instance creation process for the Employee class. I import this class to my current location before I write a test for it. This test is useful because if you cannot create the instance of the class, there is no use for it. Demonstration 3 I have created the file test_employee_class.py which has the following content: import unittest from employee_module import Employee class TestEmployee(unittest.TestCase): """ Tests for Employee class in employee_module """ def test_employee_creation(self): """ Can we store the Employee details correctly? """ emp_john = Employee('John','E001') self.assertEqual(emp_john.name, 'John', 'Employee name does not match.') self.assertEqual(emp_john.emp_id, 'E001', 'Employee ID does not match.') self.assertEqual(emp_john.company, 'Abc Ltd.', 'The company information is not found.') if __name__ == '__main__': unittest.main() Output Here is a sample output from my computer: Testing started at 11:05 ... E:\MyPrograms\Python\PythonCrashCourse\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.1\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path E:/MyPrograms/Python/PythonCrashCourse/chapter12/testing_employee_class.py Launching unittests with arguments python -m unittest E:/MyPrograms/Python/PythonCrashCourse/chapter12/testing_employee_class.py in E:\MyPrograms\Python\PythonCrashCourse\chapter12 Ran 1 test in 0.003s OK Process finished with exit code 0 EXERCISE E12.1 You saw that the Employee class in employee_module has a method get_details() which simply prints some information. Let us change the method with a return statement as follows (I kept the old codes commented for your reference): def get_details_modified(self): """ Modifying this method with return value for testing in Chapter-12 """ #print("The current employee details are as follows:") #Formating the output using f-string #print(f"Name:{self.name},Id:{self.emp_id},Company: {self.company}") return f"Name:{self.name},Id:{self.emp_id},Company: {self.company}" Can you write a test to verify this modified method in the Employee class? E12.2 Can you write a test case to test some in-built Python functions? Solution to Exercises E12.1 import unittest from employee_module import Employee class TestEmployeeMethod(unittest.TestCase): def test_get_details_modified(self): """ Does the get_details() work correctly? """ emp_kate = Employee('Kate', 'E002') self.assertTrue('Kate' in emp_kate.get_details_modified(), 'The employee name is not found.') self.assertTrue('E002' in emp_kate.get_details_modified(), 'The employee id is not found.') self.assertTrue('Abc Ltd.' in emp_kate.get_details_modified(), 'The company information is not found.') if __name__ == '__main__': unittest.main() E12.2 import unittest from math import sqrt, floor, gcd class TestEmployeeMethod(unittest.TestCase): def test_inbuilt_functions(self): """ Do the methods sqrt(),floor(), and gcd() correctly? """ self.assertEqual(5.0, sqrt(25), 'The sqrt(25) does not give the expected result.') self.assertEqual(100, floor(100.2), 'The floor(100.2) does not give the expected result.') self.assertEqual(2, gcd(4, 10), 'The gcd(4,10) does not give the expected result.') if __name__ == '__main__': unittest.main() Index A absolute path, 212, 213 Abstraction, 239, 241 Arguments, 154 assertEqual, 295, 296, 297, 299, 300, 301 assertFalse, 295 assertNotEqual, 295 assertTrue, 295, 304 Associativity, 39 B break Statement, 103 C Case Study, 47, 74, 91, 113, 148, 178, 188, 189, 191, 192, 194, 196, 198, 209, 243 Class, 187, 236, 241, 245, 278, 301 Class Methods, 278 Comments, 15, 16, 46 continue Statement, 106 Contradictions, 82 D Default Attributes, 257 Default Values, 154 Demonstration, 10, 13, 14, 16, 20, 68, 75, 76, 78, 79, 80, 82, 83, 84, 97, 99, 101, 104, 106, 108, 151, 156, 158, 159, 161, 165, 167, 187, 191, 193, 195, 199, 201, 215, 217, 219, 221, 222, 223, 225, 248, 251, 253, 255, 258, 261, 263, 264, 270, 273, 275, 282, 296, 297, 301 Dictionaries, 135 E elif, 75, 78, 79, 80, 81, 82, 83, 84, 86, 88 else, xxiii, 23, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 111, 173, 175, 193, 195, 196, 198, 207, 227, 228, 231, 275 Encapsulation, 238, 241 F FileNotFoundError, 219, 225, 226, 227, 228, 229 float, 18, 19, 20, 22, 23, 24, 59, 60, 70, 71, 79, 80, 83, 84, 110, 178, 179, 236 for Loop, 98 f-strings, 60, 63 Function Arguments, 153 Functions, 149, 293, 294 I Identity Operator, 266 IDLE, xxii, 3, 4, 5, 6, 12, 50 if-else, 75, 76, 78, 79, 83 inheritance, 269 Inheritance, 239, 241, 269 Instance, 247, 253 int, 18, 19, 20, 22, 24, 25, 59, 60, 61, 71, 77, 85, 88, 130, 188, 189, 191, 192, 193, 194, 195, 197, 198, 200, 205, 206, 229, 236 Iteration, 93 K Keyword, 154, 223 L Lambda, 160 Lists, 114 M Membership, 29, 40 Modules, 149, 162 N name mangling, 278 Numbers, 18, 19, 58 numpy, 168 O Objects, 236, 241, 245, 246 Operators, 15, 28, 29, 31, 32, 33, 34, 38, 39, 40, See P pass Statement, 195 pip, xi, 168 Polymorphism, 240, 241 Positional, 153 Positional Argument, 153 PyCharm, xxi, xxiii, 5, 6, 7, 9, 10, 11, 12, 53, 62, 63, 126, 150, 183, 184, 295, 298, 299, 302 Q Q&A Session, 241 R Relative paths, 212 remove, 24, 37, 120, 121, 122, 132, 144, 159, 218, 222, 226 rename, 222, 226 S Sets, 138, 139 Solution, 43, 70, 88, 109, 110, 144, 171, 204, 227, 288, 304 Solution to Exercises, 43, 70, 88, 109, 171, 204, 227, 288, 304 Static Methods, 278 Strings, 18, 49 T Tautology, 82 Testing, 16, 125, 292, 294, 298, 299, 301, 302 Tuples, 131, 134 V Variables, 15, 17, 27, 28 W while Loop, 94 Z zone, xviii