Final Book Vol I
Final Book Vol I
net/publication/381738595
CITATIONS READS
0 7
2 authors:
All content following this page was uploaded by Goutam Majumder on 27 June 2024.
ISBN:
DEDICATION
This book is dedicated to all the curious minds who find joy in the pursuit of knowledge and to the
educators who light the path. May this text serve as a beacon for those embarking on their journey in
computer science.
CONTENTS
Acknowledgments I
1 Introduction 1
4 Operators 51
5 Strings 78
8 Functions 184
ACKNOWLEDGMENTS
The First author extends his heartfelt gratitude to their family and friends for their unwavering support
and encouragement throughout the writing of this book. Special thanks to my friend Mr. Salil Batra,
Assistant Prof., at the Department of Computer Science of Lovely Professional University for his
valuable insights and feedback.
I am also immensely grateful to the students and educators in the field of computer science whose
questions and experiences have greatly influenced this work. Their enthusiasm and dedication to
learning have been a constant source of inspiration.
Lastly, I would like to acknowledge the contributions of the technical reviewers whose meticulous
critiques have helped enhance the quality and accuracy of this text.
i
1 INTRODUCTION
Before we deep dive into the Pythonic content of this chapter, we must go back to the history of
computer programming language or computer language. First, we need to know why a computer
programming language is required. Although some of us already know the answer is very simple “To
automate a task” or “To reduce the workload of a human”. For this, the first foundation of
computer languages was started in the early 1940s and since then many programming languages have
been designed with some specific purpose. The details of all computer languages other than Python are
discussed in the first section of this chapter. The remaining part of the chapter belongs to Python and
is sub-categorized as follows:
▪ The Section Why Python? highlights the intention of the Python developer along with language
features.
▪ Next Section Downside of Python provides the real drawback of this language, as compared with
other languages.
▪ Third Section What Python can do? highlights some of the important tasks, where Python is
mostly used.
▪ The complete evaluation of Python language is highlighted in the next Section namely Evaluation
of Python
▪ The most used Python libraries are listed in Section Popular Python Libraries so that any reader
can target any specific library for his purpose of learning this language.
▪ Finally, the last Section Python Implementations includes the details of all the implementations
of Python till today.
1
https://en.wikipedia.org/wiki/Colossus_computer
1
Python Fundamentals: A Functional Approach
their circuit or by setting banks of physical controls. After a few years, the first-generation (1GL)
computer language was designed in the form of machine language, where each instruction is written in
numeric form and executed the instructions in the hardware directly.
The second-generation languages (2GL) are also very close to machine architecture known as
assembly language. These assembly languages are more human-readable form than 1GL and relieve the
programmer of tedious and error-prone address calculations. In the 1950s between 1943 and 1945 the
first high-level language Plankalkül was written by Konrad Zuse for the German Z3 computer.
In 1949 Khon Mauchly’s Short Code2 was the first high-level language developed for computers.
Short Code statements are close to machine code represented in mathematical expression and much
more in understandable form.
The first high-level compiled language Autocode was founded by Alick Glennie at the University of
Manchester in 1952. A compiler is a system program used to translate a high-level instruction to machine
language. The second auto code was designed by R. A. Brooker in 1954 known as Mark 1 Autocode.
Both of these Autocodes were designed to work with Mark 1 computers only. The required auto code
for the EDSAC3 2 computer was founded by D. F. Hartley at the Mathematical Laboratory of the
University of Cambridge. Similar to this a separate auto code for the Atlas 1 machine was founded at
the University of Manchester.
The most popular general-purpose high-level programming language called FORTRAN4 was
founded in 1954 at IBM by Jhon Backus. FORTRAN is still popular for high-performance computing
and used for programs that benchmark and rank the world’s fastest supercomputers.
Another early general purpose language FLOW-MATIC5 was founded by Grace Hopper and
specifically designed to program in the first electronic computer UNIVAC I. Grace and her team
members found it difficult to process business data related to customers using mathematical notations
and in early 1955 they came up with specification for an English programming language and FLOW-
MATIC become publicly available in 1959.
The increased use of high-level languages introduced another requirement in the programming
paradigm. At this point in the early 1960s, there was a requirement for low-level system programming
languages, which provided facilities to perform tasks that required direct access to hardware facilities
with high-level control structures with error checking. Between the periods of 1960s to 1970s, most of
the languages are now in use. Some of the present-era languages are inspired by them and some are
limited in use. Table 1.1 provides a list of all such computer languages that exist at present along with
their original purpose at the time of development and the programming paradigm they support.
Table 1.1 List of most popular programming languages with their purpose and programming paradigm
2
https://en.wikipedia.org/wiki/Short_Code_(computer_language)
3
https://en.wikipedia.org/wiki/EDSAC
4
https://en.wikipedia.org/wiki/FORTRAN
5
https://en.wikipedia.org/wiki/FLOW-MATIC
2
Python Fundamentals: A Functional Approach
of information
PASCAL Used to develop dynamic and recursive Imperative and structured 1970
data structures
Shell script designed to be run by a Unix shell, a Scripted event 1971
command-line interpreter.
Prolog Used for logic building in artificial Supports only logic building 1972
intelligence along with theorem proving,
expert systems, term rewriting, type
systems, and automated planning.
C use in operating systems, device drivers, Multi-paradigm as procedural and 1972
and protocol stacks, though decreasingly structured
for application software.
SQL The domain-specific language used in Supports declarative paradigm 1974
RDBMS for data management and
manipulating
C++ Mostly used for system programming Supports procedural, imperative, 1979
and embedded systems, resource- functional, object-oriented,
constrained software, and large systems generic, modular
in mind, with performance, efficiency,
and flexibility.
Ada Originally designed for embedded and Supports structured, imperative, 1980
real-time systems. object-oriented, aspect-oriented,
concurrent, array, distributed,
generic, procedural, meta
MATLAB Mostly used for numeric computing. It functional, imperative, procedural, 1984
allows the design of user interface and object-oriented, Array
plotting of data as well.
J Is an array programming language mostly Supports functional programming 1990
used for mathematical and statistical via its tactic programming feature.
programming suitable for matrices Also be used for both object and
operations. prototype-based programming.
Python Discussed later in this Chapter object-oriented, procedural, 1991
functional, structured, reflective
R Mostly used for data mining, procedural, object-oriented, 1993
bioinformatics, statistics for data analysis functional, reflective, imperative,
Array
HTML For web development for webpage Markup language 1993
design
Java It is a compiled language designed to structured, imperative, object- 1995
write once and run anywhere. Mainly oriented, aspect-oriented,
used for client-server web applications. concurrent, array, distributed,
generic, procedural, meta
3
Python Fundamentals: A Functional Approach
This list is taken from IEEE Spectrums6, which ranked top performing programming languages. At
the time of writing this book some new languages are added or removed from this list. The key point
that we need to note down from the evaluation of programming languages is, that every language needs
to evolve with time and must add some unique programming features as compared to existing languages
with rich library support.
Most important we need to note that at the time of writing this chapter Python is the most trading
language followed by Java, C, JavaScript, and C++ as the top five. As of now, SQL is leading the Job
market followed by Java, Python, JavaScript, and C#. I hope that as a writer of this book, I can answer
this question why Python? Explained in the next Section.
Being an instructor for programming languages over the past 10 years my suggestion for all the
readers is to go for a Python computer language with a goal and if you find the answer to why then it's
time to start learning that language by writing small programs. Remember any computer language can’t
be fitted in just one book or syllabus, you have to go beyond that.
6
https://spectrum.ieee.org/
4
Python Fundamentals: A Functional Approach
7
https://en.wikipedia.org/wiki/Zen_of_Python
5
Python Fundamentals: A Functional Approach
▪ It only executes one thread at a time: Python has a Global Interpreter Lock that only lets one
thread execute at a time, so if you're working on a multi-threaded CPU-bound program, it'll likely be
even slower. Using multiprocessing programs instead of multithreaded programs can be an effective
workaround.
▪ It isn't mobile native: Python can be effectively and easily used for mobile purposes, but you'll need
to put a bit more effort into finding libraries that give you the necessary framework. Some examples
include Kivy, which lets you use the same API to create mobile apps and software that you can run on
Raspberry PI, Linux, and Windows.
▪ It uses a large amount of memory: If you're working on a project where many objects are active
in RAM, this could present an issue for you. Switching to NumPy could be an effective workaround to
reduce the amount of memory Python uses for each object.
6
Python Fundamentals: A Functional Approach
Table 1.2 Details of Python Releases with various versions and dates
7
Python Fundamentals: A Functional Approach
Python 2.7 was the last release in 2.x series. In November 2014, it was announced that till 2020 the
supports were provided and the developers are encouraged to move 3.0 as early as possible. Python 2.7
support ended on January 1, 2020, along with code freeze of 2.7 development branch. A final release,
2.7.18, occurred on April 20, 2020, and included fixes for critical bugs and release blockers. This marked
the end of the life of Python 2.
Python 3.0 also known as Python 3000 or Py3K comes with the most necessary changes required in
the language. At the time of release, it was not easy to retain the full backward compatibility with 2.x
series. So, it broke the compatibility with Python 2 and much Python 2 code does not run unmodified
on Python 3. Python supports dynamic typing and combines with the plan to change the semantics of
certain methods, making it difficult for mechanical translation from 2.x to 3.0. Some of the translations
are done automatically using a tool called 2to3.
Before the roll-out of Python 3.0, projects running in 2.x need to provide two different releases of
all such projects. One version is completely written in 2.x for those systems running in Python 2.x and
for the systems running in 3.x need to produce a release using 2to3.
Table 1.3 List of Top ranked Python modules used for various purposes
Python /mechanize/
Scrapy Used to extracting the data that you need https://scrapy.org/
from websites
Pyquery allows you to make jquery queries on xml https://pythonhosted.org/pyquery
documents /
Selenium is a suite of tools used to automate a https://www.selenium.dev/
browser
Requests Requests allows you to send HTTP/1.1 https://pypi.org/project/requests/
requests extremely easily.
Googlemaps Provides google map API services https://pypi.org/project/googlema
ps/
OpenCV Used for computer vision and machine https://opencv.org/
learning with artificial intelligence
Scikit-Image Image processing library https://scikit-image.org/
Scipy Used for mathematical and scientific https://scipy.org/
computation
NumPy Library for numerical analysis https://numpy.org/
matplotlib Specialized for 2-d plots as a multi- https://matplotlib.org/
platform data visualization on NumPy
arrays
TensorFlow Used for deep learning and data science https://www.tensorflow.org/
projects, developed by the Google Brain
team
Pandas Widely used Python libraries for Data https://pandas.pydata.org/
Science
Scikit-learn The machine-learning library provides a https://scikit-learn.org/stable/
variety of useful machine-learning
algorithms, and it is designed to be
interpolated into SciPy and NumPy.
Keras Used for neural network modules, similar https://keras.io/
to TensorFlow
PyTorch Created by Facebook AI for scientific https://pytorch.org/
computing package relies on the power
of GPU.
Theano A numerical computation Python library https://github.com/Theano/Thea
specifically developed for machine no
learning and deep learning.
Microsoft CNTK The open-source deep-learning library is https://learn.microsoft.com/en-
used to implement distributed deep us/cognitive-toolkit/
learning and machine learning tasks.
NLTK Best python library for Natural Language https://www.nltk.org/
Processing
10
Python Fundamentals: A Functional Approach
▪ Stackless Python is a significant fork of CPython that implements microthreads; it does not use the
call stack in the same way, thus allowing massively concurrent programs [4].
▪ For microcontroller including Lego Mindstorms EV3, Micro Python and Circuit Python are the two
Python 3 variants [5].
▪ Pyston is a variant of the Python runtime that uses just-in-time compilation to speed up the
execution of Python programs [6].
▪ Cinder is a performance-oriented fork of CPython 3.8 that contains a number of optimizations
including bytecode inline caching, eager evaluation of coroutines, a method-at-a-time JIT, and an
experimental bytecode compiler.
In addition to the above list, it also has some other implementations designed as cross compiler to
other languages. These includes:
▪ Brython8, Transcrypt9 and Pyjs compile Python to JavaScript.
▪ Cython compiles (a superset of) Python 2.7 to C and the resulting code is also usable with Python 3.
▪ Nuitka10 compiles Python into C.
▪ Numba11 uses LLVM to compile a subset of Python to machine code.
▪ Pythran12 compiles a subset of Python 3 to C++11 only.
▪ RPython13 can be compiled to C and is used to build the PyPy interpreter of Python.
▪ MyHDL14 is a Python-based hardware description language (HDL), that converts MyHDL code to
Verilog or VHDL code.
These are some older implementations that didn’t support any of the Python 3.x syntax. Such as:
▪ Google's Grumpy15 with latest release in 2017, translates Python 2 to Go.
▪ IronPython16 allows running Python 2.7 and 3.4 programs on the .NET Common Language
Runtime.
▪ Jython17 compiles Python 2.7 to Java bytecode, allowing the use of the Java libraries from a Python
program.
▪ Pyrex18 and Shed Skin compile to C and C++ respectively.
********************END OF CHAPTER********************
8
https://brython.info/
9
https://www.transcrypt.org/
10
https://nuitka.net/
11
https://numba.pydata.org/
12
https://pythran.readthedocs.io/en/latest/
13
https://pypi.org/project/rpython/
14
https://myhdl.org/
15
https://github.com/google/grumpy
16
https://ironpython.net/
17
https://www.jython.org/
18
https://www.csse.canterbury.ac.nz/greg.ewing/python/Pyrex/
12
2 SETTING UP AND GETTING STARTED WITH PYTHON
This chapter shows the required steps to make your system ready and to write Python scripts. Sections
of this chapter are segregated as follows:
▪ First check your system comes with any version of Python
▪ Install or update Python on Windows, macOS, and Linux
▪ Use Python on mobile devices like phones or tablets
▪ Use Python on the Web with online interpreters
Before start to install, it is better to check if you already have Python on your Windows machine, first
open a command-line application, such as PowerShell.
Instead of PowerShell alternately you can also use cmd.exe or windows Terminal. With the command
line open, type in the following command and press Enter :
It seems Python is not available in your system, and you can find three ways to install the official Python
distribution on Windows:
▪ Microsoft Store package: The most straightforward installation method on Windows involves
13
Python Fundamentals: A Functional Approach
installing from the Microsoft Store app. This is recommended for beginner Python users
looking for an easy-to-set-up interactive experience.
▪ Full Installer: This approach involves downloading Python directly from the Python.org
website. This is recommended for intermediate and advanced developers who need more
control during the setup process.
If you’re new to Python and looking to get started quickly, then the Microsoft Store package is the best
way to get up and running without any fuss. You can install from the Microsoft Store in two steps.
▪ Open the Microsoft App Store, click from Taskbar or alternately type python in PowerShell
and press Enter . The Microsoft Store will automatically launch and take you to the latest
version of Python in the store. Select the most latest version that you can see or at least Python
ver. 3.10, as all illustrations are tested on this version.
▪ Install the Python App and to do that click over Install. You can see downloading is start and
wait for it. Make sure that the Python application you’ve selected is created by the Python
Software Foundation.
▪ To check Python is installed or not type the given command python –version in the
PowerShell. You can see the installed version, if installation is completed.
14
Python Fundamentals: A Functional Approach
▪ Open a browser window and navigate to the Python.org Downloads page for Windows.
▪ Under the Download Tab select Windows and till the writing of this chapter you can see latest
stable version is 3.12.2. If you want any specific version, check the right side of the page as Pre-
releases heading.
▪ Now select either Windows Installer 64-bit executable installer for 64-bit or Windows Installer
32-bit for 32-bit processor architecture.
You can also complete the installation on Windows using alternative distributions, such as Anaconda,
but this chapter covers only official distributions. Anaconda19 is a popular platform for doing scientific
computing and data science with Python. To download you can visit anaconda download page
https://www.anaconda.com/download.
In this section, you’ll learn how to install Python using Ubuntu’s apt package manager. Depending on
the version of the Ubuntu distribution you run, the process for setting up Python on your system will
vary. You can determine your local Ubuntu version by running the following command:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04.4 LTS
Release: 16.04
Codename: xenial
Follow the instructions below that match the version number you see under Release in the console
output. Ubuntu 18.04, Ubuntu 20.04 and above, doesn’t come by default Python 3.8, but it is available
in the Universe repository. To install version 3.8, open a terminal application and type the following
commands:
$ sudo apt-get update
$ sudo apt-get install python3.8 python3-pip
Once the installation is complete, you can run Python 3.8 with the python3.8 command and pip with
the pip3 command.
Like Windows, in macOS also you can install it from direct installer by navigate to the Python.org
Downloads page for macOS. Select any specific version under Pre-releases heading and download and
install it. Before that, it is better to check any existing installer in available or not. To check which Python
version you have on your Mac, first open a command-line application, such as Terminal. To open the
19
https://www.anaconda.com/
15
Python Fundamentals: A Functional Approach
Alternatively, you can open Finder and navigate to Applications → Utilities → Terminal. With the
command line open, type in the following commands:
# Check the system Python version
$ python --version
# Check the Python 2 version
$ python2 --version
# Check the Python 3 version
$ python3 --version
If you have Python on your system, then one or more of these commands should respond with a version
number.
If you want to try out the examples in this tutorial without setting up Python on your machine, then
there are several websites that offer an online Python interpreter:
▪ Python.org Online Console
▪ Repl.it
▪ Python Fiddle
▪ Trinket
▪ Python Anywhere
These cloud-based Python interpreters may not be able to execute some of the more complex examples
in this tutorial, but they’re adequate for running most of the code and may be a nice way to get started.
More information on using these sites is presented in the next tutorial in this series.
There are many ways you can execute a python script; these are as follows:
16
Python Fundamentals: A Functional Approach
Now you can type any python command in the command prompt (>>>) and check the correct
outcome, as shown below:
You can use any text editor for writing your script and save the file with .py extension.
Let’s assume that we have save the file a first.py in inside a folder Python code of D:\ drive.
Now open any code editor such as Notepad++ and type python script and save it in location
as D:\Python code\
Now open PowerShell and set the path of the PowerShell to the desired location.
Now type the command as python filename.py in our case python first.py
17
Python Fundamentals: A Functional Approach
If you follow the guide of python installation as discussed in Section 2.1 you can use Python
Integrated Development and Learning Environment (IDLE) to write and execute any python script.
Follow the steps given below:
Open the IDLE by clicking over the IDLE icon or search using windows search bar. Once it is open it
will be look like this:
You can directly type any python command after the Python command line (>>>), as shown below:
If you are learning any programming language for the first time, it is better to write python
command through script file. To open a new script file, you can open it by clicking over File
menu -> New File.
18
Python Fundamentals: A Functional Approach
To execute the script Click Run→ Run Module or Press F5 and output will display in the IDLE
window.
****************END OF CHAPTER****************
19
3 GETTING FAMILIAR WITH PYTHON
Before start writing extensive Python scripts, this chapter gently helps you to understand the importance
of basic Python programming components. It starts with the explanation and limitations of a pre-defined
set of words called keywords. Although we have made this discussion very limited about the keywords
you will get to know more about them according to their need in later chapters.
After that, we have explained two other programming tools that are important for documentation
purposes such as Naming Conventions for identifiers and Commenting a Python script. Mostly these
two tools are required by the Python developer which improves code readability and acceptance among
other developers.
The next section describes Python Types deeply discussed various data types also known as Objects
either as pre-defined or created with their uses. Later in the Sections Object Types, Input and Output
and Variable Assignments discussed how to use these types either with directly assigned values or by
getting values from the user as input and producing output by manipulating them.
Finally, the last Section highlights Built-in Functions comes with python interpreter. In this chapter
based on the necessity, we have also imported some of the modules in code examples using import
statement. But we have not discussed much about importing a module in this Chapter, rather we keep
it separately in Chapter 8.
3.1 Keywords
Every programming language has special reserved words, or keywords, that have specific meanings
and restrictions around how they should be used. Python is no different. Python keywords are the
fundamental building blocks of any Python program.
Python keywords are special reserved words that have specific meanings and purposes and can’t be
used for anything but those specific purposes. These keywords are always available—you’ll never have
to import them into your code.
Python keywords are different from Python’s built-in functions and types. The built-in functions and
types are also always available, but they aren’t as restrictive as the keywords in their usage.
An example of something you can’t do with Python keywords is assign something to them. If you
try, then you’ll get a SyntaxError. You won’t get SyntaxError, if you try to assign something to a built-
in function or type, but it still isn’t a good idea. As of Python 3.10, there are thirty-five keywords in
Python. A list of keywords is given in Table 3.1.
20
Python Fundamentals: A Functional Approach
The list of keywords is also retrieved using help in-built functions by passing ‘keywords’ as a
parameter to it. Consider the code snippet given below:
>>>help('keywords')
ℎ𝑒𝑙𝑝() function can be used to obtain more information about any keyword, by passing that keyword
as argument to it, as follows:
>>> help('pass')
Python offers a module namely keyword to check a given string is a keyword or soft keyword (added
in Python 3.10). Soft keywords are only reserved for specific contexts and identifiers match, case and
_ (underscored sign) can syntactically act as a keyword’s context related to pattern matching. Table 3.2
highlights the details of keyword module.
given below:
>>>import keyword
>>>keyword.softkwlist
['_', 'case', 'match']
>>>keyword.iskeyword('is')
True
>>>keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break',
'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from',
'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass',
'raise', 'return', 'try', 'while', 'with', 'yield']
>>>keyword.issoftkeyword('match')
True
These are set of rules for choosing the character sequence to be used for the naming of identifiers such
as variables, constants, types, functions, and other entities in source code and documentation. For the
following reasons naming conventions are followed by the programmers:
▪ to reduce the effort to understand the source code and to avoid naming collisions of the
identifiers.
▪ to enable code reviews to focus on issues rather than focusing on the syntax and naming
standards.
▪ to provide additional information (i.e., metadata) about the use to which an identifier is put.
▪ to promote consistency within a development team, so that each one can understand other’s
code.
▪ to enable the use of automated refactoring or search and replace tools with minimal potential
for error handling.
▪ to provide a better understanding in case of code reuse after a long interval of time.
▪ In Python, a variable can have a short name (like 𝑥 and 𝑦) or a more descriptive name such as
age, carname, total_volume. A Python developer needs to follow the following rules as a set
of naming conventions according to the coding standards:
▪ A name must be start with an alphabet (A-Z, a-z) or the underscore (_);
▪ name can’t start with a number (0-9);
▪ names can be alpha-numeric but strictly start with an alphabet (A-Z, a-z) or the underscore (_)
only.
▪ names are case-sensitive in Python, which means age, Age, and AGE represent three different
identifiers.
Listing 3.1 highlights a couple of examples of valid identifier names and some of the invalid names
are listed in Listing 3.2 and using any of these will raise SyntaxError by the interpreter.
3.3 Comments
When writing code, it’s important to make sure that your code can be easily understood by others.
Giving variables obvious names, defining explicit functions, and organizing your code are all great ways
to do this. Another awesome and easy way to increase the readability of your code is by using comments!
Comments are an integral part of any program. In Python, comments come in the form of module-
level docstrings, or even inline explanations that help shed light on a complex function. Before diving
into the different types of comments, let’s take a closer look at why commenting on your code is so
important.
First, this section highlights two possible scenarios in which a programmer decided not to comment
on their code. So that you can understand the importance of proper documentation of Python code and
later you are going to know how to comment on Python scripts.
▪ Reading your own code - Client A wants a last-minute deployment for their web service. You’re
already on a tight deadline, so you decide to just make it work. All that “extra” stuff—documentation,
proper commenting, and so forth—you’ll add that later.
The deadline comes, and you deploy the service right on time and you have noted and go back to
update the comments, but before you can put it on your to-do list, your boss comes over with a new
project that you need to get started on immediately. Within a few days, you’d completely forgotten that
you were supposed to go back and properly comment on the code you wrote for Client A.
Suppose after six months Client A needs a patch built for that same service to comply with some
new requirements. It’s your job to maintain it since you were the one who built it in the first place. Now
it will be difficult to recall all the developments that you have made and after spending hours parsing
through your old code, you’re completely lost.
You were in such a rush at the time that you didn’t name your variables properly or even set your
functions up in the proper control flow. Worst of all, you don’t have any comments in the script to tell
you what’s what! Developers forget what their code does all the time, especially if it was written a long
time ago or under a lot of pressure. When a deadline is fast approaching, and hours in front of the
computer have led to bloodshot eyes and cramped hands, that pressure can be reflected in the form of
code that is messier than usual. Writing comments as you go is a great way to prevent the above scenario
from happening. Be nice to Future You!
23
Python Fundamentals: A Functional Approach
▪ Your code is being read by others - Imagine you’re the only developer working on a small Django
project. You understand your own code well, so you don’t tend to use comments or any other sort of
documentation, and you like it that way. Comments take time to write and maintain, and you just don’t
see the point.
The only problem is, by the end of the year your “small Django project” has turned into a “20,000
lines of code” project, and your supervisor is bringing on additional developers to help maintain it. The
new developers work hard to quickly get up to speed, but within the first few days of working together,
you’ve realized that they’re having some trouble. You used some unorthodox variable names and wrote
with super short syntax. The new developer spends a lot of time stepping through your code line by
line, trying to figure out how it all works. It takes a few days before they can even help you maintain it!
Using comments throughout your code can help other developers in situations like this one.
Comments help other developers skim through your code and gain an understanding of how it all works
very quickly.
Now that you understand why it’s so important to comment on your code, let’s go over some basics
so you know how to do it properly. Comments are for developers. They describe parts of the code
where necessary to facilitate the understanding of programmers, including yourself. A comment can be
any of these two types:
We can do this by putting a hash mark (#) before your desired comment. Consider the example given
below:
>>>#This is a comment
>>>print('Commenting in python')
Commenting in python
Python interpreter doesn’t interpret a line of code if the line is preceded with a # till the end of the
line. That’s why we didn’t see anything in the output from the first line of the code.
Note: Comments should be short, sweet, and to the point. While PEP 8 advises keeping code at 79
characters or fewer per line, it suggests a max of 72 characters for inline comments and docstrings. If
your comment is approaching or exceeding that length, then you’ll want to spread it out over multiple
lines.
3.3.2 Multiline Commenting
Python doesn’t have a way to write multiline comments as you can in other languages such as C, Java,
and Go. Don’t try this with your code, or else you will end with a SyntaxError.
>>># So you can't
just do this
in python
For the above code interpreter only ignores the first line as single line comment and it interprets as
wrong syntax for the others and raised a SyntaxError. While Python doesn’t have native multiline
commenting functionality, you can create multiline comments in Python in two ways. The first way is
simply by pressing the return key after each line, adding a # symbol, and continuing your comment from
there. See the example given below:
24
Python Fundamentals: A Functional Approach
While this gives you multiline functionality, this isn’t technically a comment. It’s a string that’s not
assigned to any variable, so it’s not called or referenced by your program. Still, since it’ll be ignored at
runtime and won’t appear in the bytecode, it can effectively act as a comment.
Note: Sometimes it also depending on where we are putting these three quotes ("""---""") in the code.
If we put it inside the function definition it will be the part of Python byte code. This one is known as
docstring.
In Table 3.3 three classes are categorized as sequence type, but sets, dictionary, and string can also be
considered as sequence type objects in python. It means values holding by each sequence types can be
accessed sequentially.
The class names or data types are highlighted with bold text in Table 3.3 and in next three sub-
sections numeric types are discussed broadly in Section Numeric Type and all sequence types are
formally introduced in Section Sequence Type. This discussion on strings is also limited to the
introductory part only, which is broadly discussed separately along with its various operations.
Integers
In Python integers are whole numbers i.e., no fractions, no decimal points, nothing fancy. Well, aside
from a possible initial sign and bases, if you want to express numbers in other ways than the usual
decimal (base 10). The 𝑖𝑛𝑡() constructor method can be used to create an integer object or directly a
number with possible sign can be assigned. Any sequence of digits in Python represents an integer literal
and examples of all valid integer literal forms are given in code Listing 3.3:
26
Python Fundamentals: A Functional Approach
The prefix characters are supported in upper (0b, 0o, or 0x) as well as lower (0B, 0O, or 0X) case.
We must note that while using any integer to an expression for example in 𝑝𝑟𝑖𝑛𝑡() statement at line 7
values will be always processed in decimal form, as values are printed in the output of code Listing 3.4.
Pythons have three in-built function to do that. 𝑏𝑖𝑛(), 𝑜𝑐𝑡(), and ℎ𝑒𝑥() are used to convert the decimal
form of integer to binary, octal and hexa-decimal respectively.
>>>print(bin(a), oct(c), hex(e))
0b11 0o245 0x2345a
Unlimited precision integers are built-in tools in Python and support up to memory of your device. They
are also why we can raise large exponent numbers:
>>>45 ** 40
1344313472276061687044450174711376326749814325012266635894775390625
Floating-Point
In Python real numbers are represented by the class float with fixed 8 bytes (or 64 bits) of memory.
These 64-bits are aligned into three different parts, as (a) a single bit is reserved as sign (positive or
negative) bit; (b) 11 bits are reserved for exponent 1.5𝑒 −5 1.5 × 10−5 (exponent is −5) within a range
of [-1022, 1023]; and (c) 52 bits are significant digits.
In computers floating-point numbers are represented in base 2 (binary) fractions. For example, the
decimal fraction 0.125 has value1/10 + 2/100 + 5/1000, and in the same way the binary fraction
0.001 has value 0/2 + 0/4 + 1/8. These two fractions have identical values, the only real difference
being that the first is written in base 10 fractional notation, and the second in base 2. For example,
12.345 can be represented as a base-ten floating-point number:
𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡
⏞
−3
12.45 = 12345
⏟ × 10
⏟
𝑠𝑖𝑔𝑛𝑖𝑓𝑖𝑐𝑎𝑛𝑑 𝑏𝑎𝑠𝑒
The constructor method 𝑓𝑙𝑜𝑎𝑡() of the float class returns a floating-point number based on the
number or a string:
>>>float(3.45)
3.45
>>>float('3.45')
3.45
In computers there are very few factional numbers represented as binary fractions, instead an
27
Python Fundamentals: A Functional Approach
approximated binary floating-point number is stored into memory. Consider the fraction 1/3, in decimal
base 10 it can be 0.3 or 0.33 or 0.333 and so on. No matter how many digits you’re willing to consider,
the result will never be same. Let’s do a comparison between two float numbers:
>>>x = 0.1 + 0.1 + 0.1
>>>y = 0.3
>>>x==y
False
It looks very strange, variables x and y having same value but comparison states different story. This
is because Python cannot use a finite number of digits to represent the numbers. Let’s check this using
𝑓𝑜𝑟𝑚𝑎𝑡() function:
>>>format(x,'.20f')
'0.30000000000000004441'
>>>format(y,'.20f')
'0.29999999999999998890'
Here we have taken upto 20 decimal place and exactly the numbers are not stored into computer
memory while using binary decimal. Just remember, even though the printed result looks like the exact
value of 0.3, the actual stored value is the nearest representable binary fraction.
Note: This is in the very nature of binary floating-point: this is not a bug in Python, and it is not a bug
in your code either. You’ll see the same kind of thing in all languages that support your hardware’s
floating-point arithmetic (although some languages may not display the difference by default, or in all
output modes).
PEP48520 provides a solution that fixes this problem by using relative and absolute tolerances. It
provides isclose() function of math module and returns True if two numbers are relatively close to
each other:
>>>x = 0.1 + 0.1 + 0.1
>>>y = 0.3
>>>from math import isclose
>>>isclose(x,y)
True
Some of these drawbacks are handled using Decimal and Fraction data types are discussed in the next
two sections.
Decimal
Decimals are like floating-point numbers with fixed precisions. It was introduced in Python 2.4 to core
numeric type as decimal object, formally known as Decimal. Like floating-point type we can’t just assign
any literals to a name, for that we need to import the decimal module. After all we need to answer why
another numeric type like floating-point. Let’s first list all the advantages of decimal type over floating-
point type:
▪ Decimal numbers can be represented exactly. In contrast, numbers like 1.1 and 2.2 do not have
exact representations in binary floating point. End users typically would not expect 1.1 + 2.2 to
display as 3.3000000000000003 as it does with binary floating point.
20
https://peps.python.org/pep-0485/
28
Python Fundamentals: A Functional Approach
▪ The exactness carries over into arithmetic. In decimal floating point, 0.1 + 0.1 + 0.1 - 0.3 is
exactly equal to zero. In binary floating point, the result is 5.5511151231257827e-017, near to
zero. For this reason, decimal is preferred in accounting applications that have strict equality
invariants.
▪ The decimal module incorporates a notion of significant places so that 1.30 + 1.20 is 2.50. The
trailing zero is kept indicating significance. This is the customary presentation for monetary
applications. For multiplication, the “schoolbook” approach uses all the figures in the
multiplicands. For instance, 1.3 * 1.2 gives 1.56 while 1.30 * 1.20 gives 1.5600.
To create a decimal type need to import the decimal module first and need to go through the current
context with 𝑔𝑒𝑡𝑐𝑜𝑛𝑡𝑒𝑥𝑡() and if necessary we can set new values to its parameters:
>>>from decimal import *
>>>getcontext()
The decimal number supports up to 28 decimal places and Python interpreter raises exception on
getting signals of InvalidOperation, DivisionByZero and Overflow. The context for arithmetic is an
environment specifying precision, rounding rules, limits on exponents, flags indicating the results of
operations, and trap enablers which determine whether signals are treated as exceptions. Table 3.4 lists
all the rounding options of decimal module:
On passing a floating-point value, it will take it up to the precision value of the current context:
29
Python Fundamentals: A Functional Approach
>>>decimal.Decimal(3.68)
Decimal('3.680000000000000159872115546022541821002960205078125')
Unlike hardware based binary floating point, the decimal module has a user alterable precision
(defaulting to 28 places) and can be altered as follows:
>>>getcontext().prec=3
>>>Decimal('1')/Decimal('7')
Decimal('0.143')
If the FloatOperation signal is trapped, accidental mixing of decimals and floats in constructors or
ordering comparisons raises an exception:
>>>getcontext().traps[FloatOperation] = True
>>>Decimal(5.7)
Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
Decimal(5.7)
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
The significance of a new Decimal is determined solely by the number of digits input. Context
precision and rounding only come into play during arithmetic operations only, not at the time of
creation.
In Listing 3.5, precision is set to 6, it doesn’t make any impact on the next two Decimal creations. It
considers all the digits those are passed to 𝐷𝑒𝑐𝑖𝑚𝑎𝑙() constructor built-in function. But the addition
operations of line 6 and 9 produces the result within the length of precision and the second expression
rounding up the result up to 6th decimal place.
Fractional
The shortcoming of floating-point arithmetic in binary is address by Fraction data type. It’s not coming
as another built-in types like int or float rather we need import it from fractions module. If we need to
work with financial data and require infinite precision for some calculation it will be preferred over
floating-point type. Using Fraction class we can create many ways a fraction object depending on
number of values that we are passing to Fraction() constructor method of fractions class. The class
constructor accepts zero, one, or two arguments of various types:
30
Python Fundamentals: A Functional Approach
The first version in Listing 3.6 simply returns 0 as factional number. The second variant of
constructor takes 1 argument and returns 3 as Fractional number. The last variant takes two numbers as
numerator and denominator. The third variant only accepts instances of rational number.
Note: To get the fractional value in human-friendly texture (with slash / character) it is necessary to
print() the Fraction object.
For the third variant of Decimal() constructor, when we pass a valid numerator and denominator,
they will be normalized if they have a common divisor:
>>>fractions.Fraction(3,6) #gcd(3,6) = 3
Fraction(1, 2)
>>>fractions.Fraction(0,5) #gcd(0,5) = 5
Fraction(0, 1)
The normalization also takes the minus sign into account when you define negative fractions:
>>>-fractions.Fraction(3,12)
Fraction(-1, 4)
>>>fractions.Fraction(-3,12)
Fraction(-1, 4)
>>>fractions.Fraction(3,-12)
Fraction(-1, 4)
The second variant constructor of Listing 3.6 also takes arguments of real and string types. From real
types it accepts both floating-point and decimal number and both considered irrational number.
>>>print(fractions.Fraction(0.5))
1/2
>>>print(fractions.Fraction('3'))
3
For the floating-point type of result is the same in fractional notation. However, this code works as
expected only by coincidence. In most cases, you won’t get the intended value due to the representation
error that affects float numbers, whether they’re rational or not. For example, a fraction 1/10 can be
accommodated as 0.1 in floating-point number. But in python story is different, because of binary
representation of 0.1 stores an approximate value in memory.
>>>print(fractions.Fraction(0.1))
3602879701896397/36028797018963968
The previous number 0.5 can be accommodated in memory, so it gets the exact representation of
fraction 1/2. But 1/10 can only be approximated with a non-terminating repeating expansion of binary
digits. The numerator and denominator of a Fraction object can be access separately using numerator
and denominator member of Fraction object:
31
Python Fundamentals: A Functional Approach
>>>a = fractions.Fraction(3,4)
>>>print(a.numerator, a.denominator)
3 4
We have seen how to convert other data types to Fraction type. Converting between built-in data types
in Python can be done by calling constructor methods int(), float(), str() and complex(). A
fractional type can be directly converted to float type and internally it’s done by dividing the numerator
by denominator of the fraction.
>>>a = fractions.Fraction(3,4)
>>>float(a)
0.75
>>>str(a)
'3/4'
>>>complex(a)
(0.75+0j)
Fraction didn’t provide any implementation directly to convert to integer. It simply truncates the fraction
to smallest integer and like converting a float to an integer:
>>>a = fractions.Fraction(22,3)
>>>print(a)
>>>22/3
>>>int(a) #truncates the integer part
7
Converting to Decimal type will raise a TypeError, since Fractions represents a division and to bypass
this, we can convert any part as Decimal and dividing them manually:
>>>a = fractions.Fraction(12,3)
>>>d = a.numerator / Decimal(a.denominator)
>>>d
Decimal('4')
32
Python Fundamentals: A Functional Approach
the primary use for fractions is to represent rational numbers, so they might be less convenient for
storing money than decimals.
There are two advantages to using Fraction over Decimal. The first one is infinite precision bounded
only by your available memory. This lets you represent rational numbers with non-terminating and
recurring decimal expansion without any loss of information:
Multiplying 1/3 by 3 gives you exactly 1 in the fractional notation, but the result is rounded in the
decimal notation.
Complex Number
Complex numbers have two parts, real and imaginary (imag) part. It represents in the form A+Bj, where
A and B representing real and imag part respectively. A complex number is created by passing two real
numbers in constructor complex().
>>>c = complex(4,5)
>>>c, type(c)
((4+5j), <class 'complex'>)
complex() is a built-in function a constructor method of class Complex and returns the number in the
form A+Bj, if two numbers are passed, else A+0j, if only one real number is passed.
>>>complex(3)
(3+0j)
It is also possible without using complex() function still a complex number can be formed by satisfying
the format A+Bj:
>>>c = 4+7j
>>>c
(4+7j)
Postfix a character 'j' or 'J' with a numeric literal yields an imaginary number (a complex number with a
zero as real part). This imag part can add with an integer or float to from a complex number with real
and imag part, as given in code Listing 3.8:
33
Python Fundamentals: A Functional Approach
It is also possible to access the real and imag part of a complex number separately using the real and
imag attribute of a complex object:
>>>c = 4+6j
>>>c.real, c.imag
(4.0, 6.0)
Note: For the type of consistency in Python each part of the complex number is considered as float,
because absolute value of a complex number is floating type. So, there is no reason to make a special
case and return an int, if the output happens to be a round number. Also, note that the same reasoning
applies for the components of your imaginary numbers which are always stored as float.
Boolean
Python 3 has another built-in type as Boolean, which has two values as True and False. Using built-in
type() functions, it is possible to check the type of True and False:
>>>type(True)
<class 'bool'>
>>>type(False)
<class 'bool'>
An expression containing conditional, logical, identity and membership operators always evaluates to
bool type. Details about working of Boolean type expression is discussed in Chapter 4. In Python built-
in names are not keyword and we can assign them any value. But True and False are not built-in types,
they are keywords and assigning any value to them will be raised a SyntaxError, as follows:
>>>bool = 45
>>>type(bool)
<class 'int'>
>>>a = True
>>>type(a)
<class 'bool'>
>>>True = 3
SyntaxError: cannot assign to True
Booleans are considered as numeric type in Python and any value other than 0 or None, will be treated
as True. Because they are considered as numeric types, it is possible to perform arithmetic operations
with them. At any point if you want to count the number of items that satisfy a condition, means number
of True counts.
34
Python Fundamentals: A Functional Approach
>>>bool(3)
True
>>>True + True
2
>>>int(True)
1
>>>bool(56.7)
True
>>>bool(0)
False
>>>bool(None)
False
Built-in function len() returns the length of a sequence type and in Python the length of an object
considered as True:
>>>a = [3,4]
>>>len(a)
2
>>>bool(a)
True
>>>b = []
>>>len(b)
0
>>>bool(a)
False
The Python Boolean is a commonly used data type with many useful applications. You can use
Booleans with operators like not, and, or, in, is, ==, and != to compare the values and check for
membership, identity, or equality (discussed in Chapter 4). You can also use Boolean testing
with if statement (discussed in Chapter 6) to control the flow of your programs based on the truthiness
of an expression.
3.4.2 String
Discussion in this section is limited to string declaration only, details about string operations and
methods can be found in Chapter 5. In Python strings are known as collection of characters and type of
string is known as str. Strings literal is delimited using single or double quote and any number of
characters between the opening and matching closing delimiter is considered as string:
>>>str1 = 'Hello'
>>>str2 = "Python"
>>>type(str1)
<class 'str'>
In Python a string can contain any number of characters that supports your system memory and it can
be empty also by assigning a pair of single or double quotes or simply by calling str() constructor
method of string class:
35
Python Fundamentals: A Functional Approach
>>>str1 = ''
>>>str2 = str()
>>>str1, str2
('', '')
Any type can be converted to string by passing a value of type to 𝑠𝑡𝑟() constructor method inside
quotes:
>>>str('1')
'1'
>>>str('4.6')
'4.6'
If you wish to include either type of quote character within the string, the simplest way is to delimit the
string with the other type. If a string is to contain a single quote, delimit it with double quotes and vice
versa:
In Python a multiline string can be formed using any form of quotes three times. A multiline string can
include escape sequences, but single quotes, double quotes, and newlines can be included without
escaping them.
In Python sequence types supports a variety of operations as listed in Table 3.5 for both mutable and
immutable type. In Table 3.5, 𝑠 and 𝑡 are sequences of the same type, 𝑛, 𝑖, 𝑗 and 𝑘 are integers and 𝑥 is
an arbitrary object that meets any type and value restrictions imposed by 𝑠. Details about these
operations can be found in the respective chapter mentioned in 4th column of the table.
The in and not in operations have the same priorities as the comparison operations. The +
(concatenation) and * (repetition) operations have the same priority as the corresponding numeric
operations.
Discussed in
Operation Operation Type Result
Chapter
𝑥 𝑖𝑛 𝑠 Membership Return True if an item of s is x or False 4
36
Python Fundamentals: A Functional Approach
List
List is one of the built-in data structures in Python. It can accommodate multiple values of similar or
dissimilar types. If you are coming from other programming domain, Python list is like the Vectors in
C++ or ArrayList in JAVA. Lists are dynamic in nature, can grow or shrink through its insertion and
deletion operation. List also supports searching for an item using membership operator or sequentially
using a loop. Inserting and deleting the element from the beginning of the list is considered as costliest
operation as all the elements are needed to be shifted by one position. This Section is all about the
introduction to list as a data type and its assignments, detailed explanation can be found in Chapter 7.
We will begin with declaration of list and accessing of it:
>>>list1 = list()
>>>list2 = []
Like other built-in types an empty list can be created using constructor method list() or by assigning
a pair of empty square brackets ([]). List can be created by passing other sequence types (see Listing
3.9) to the constructor including strings.
>>>list('abc')
['a', 'b', 'c']
>>>list(range(3))
[0, 1, 2]
>>>d = {2,3,4} #set
>>>list(d)
[2, 3, 4]
>>>s = (1,2,3) #tuple
>>>list(s)
[1, 2, 3]
>>>di = {'a':2, 'b':3} #dictonary
>>>list(di)
['a', 'b']
37
Python Fundamentals: A Functional Approach
Numeric types aren’t iterable, and trying to do so will raised a TypeError, as follows:
>>>list(11)
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
list(11)
TypeError: 'int' object is not iterable
A list can be created using by assigned comma separated similar or dissimilar values inside a pair of
square brackets:
>>>a = [2,3,4] #similar types
>>>b = [2,5.6,7+3j,'Python'] #different types
Tuple
Tuples are immutable (means can’t alter after creation) sequence, typically used to store collections of
dissimilar data. Tuples are also used for cases where an immutable sequence of homogeneous data is
needed. Number of ways tuple can be created (see code Listing 3.10):
Constructor tuple() can take maximum one argument and creates a tuple of an iterable object of
same items and same order. Without comma at line 3 and 4, Python would consider them as single-
expression statement. At line 6 and 7, comma delimit individual elements within the parentheses,
forming tuples with multiple elements. Each comma acts as a separator, indicating that the values on
either side of it are distinct items in the tuple.
38
Python Fundamentals: A Functional Approach
1 >>>b'hello'
2 b'hello'
3 >>>my_bytes = bytes([0x48, 0x65, 0x6c, 0x6c, 0x6f])
4 >>>print(my_bytes)
5 b'Hello'
6 >>>print(type(my_bytes))
7 <class 'bytes'>
In the above example, a bytes object named my_bytes is created using the bytes() constructor and
passing a list of integers representing the ASCII codes in hexa-decimal form for the characters 'H', 'e',
'l', 'l', and 'o'. The resulting bytes object is equivalent to the bytes literal b'Hello'. A bytes object can be
created from a Unicode string using UTF-8 encoding as follows:
▪ Efficient storage of binary data: Byte arrays provide a compact and efficient way to store binary
data, such as image or audio files, network packets, or serialized objects. Because they are
mutable, you can modify the data in place without having to create a new copy of the entire
sequence.
▪ Network communication: Byte arrays can be used to represent messages or packets in network
communication protocols, such as TCP, UDP, or HTTP. They can be sent and received over a
network connection using socket programming, and they can be parsed and processed on the
receiving end.
▪ Cryptography: Byte arrays can be used to represent cryptographic keys, digests, or ciphertexts.
They can be processed using cryptographic algorithms, such as encryption, decryption, or
hashing, and they can be compared for equality or used as inputs to other cryptographic
operations.
▪ Low-level system programming: Byte arrays can be used to interact with low-level system APIs
that require binary data, such as device drivers, file I/O, or memory-mapped files. They can be
39
Python Fundamentals: A Functional Approach
passed as arguments to C functions using the ctypes module or accessed directly using the
memory view object.
Dictionary
A dictionary is a mapping object which maps hashable (means an object if it has a hash value which
never changes during the lifetime) values. It is an unordered collection of data values, and all items are
stored in a 𝑘𝑒𝑦: 𝑣𝑎𝑙𝑢𝑒 pair. A single key is associated with a value, and it is a mutable type of object.
Two ways an empty dictionary can be formed either using a pair of curly braces ( {}) or by calling
constructor method 𝑑𝑖𝑐𝑡():
>>>d1 = {}
>>>d2 = dict()
>>>type(d1)
<class 'dict'>
By passing key:value pair inside a pair of curly braces ({}), a dictionary can be form and keep in mind
dictionary keys are always immutable type, means non-modifiable objects are allowed as dictionary key.
>>>d = {'lan':'Python','ver':3.11}
>>>d
{'lan': 'Python', 'ver': 3.11}
An identifier of immutable type can be used as key-value pair to form a dictionary. The list
[('lan','Python'),('ver',3.11)] contains tuples, where each tuples represents a key-value pair.
can also be created by passing a list of tuples.
>>> dict([('lan','Python'),('ver',3.11)])
{'lan': 'Python', 'ver': 3.11}
Dictionary supports searching an item in arbitrary order and all keys in dictionary should be unique. On
adding a new item with existing key can update its value only:
Python dictionaries also support copy from another dictionary, removing a specific item based on key
or pop out the last 𝑘𝑒𝑦: 𝑣𝑎𝑙𝑢𝑒 pair item. All these operations with creation of dictionary from other
data types discussed in Chapter 7.
Sets
A set object is an unordered collection of distinct hashable objects. Common uses include membership
testing, removing duplicates from a sequence, and computing mathematical operations such as
intersection, union, difference, and symmetric difference.
Pythons have two built-in set types as set and frozenset. Out of these set is mutable type means items
can be altered later and frozenset is immutable type, can be used as a key of a dictionary. Constructor
methods like other built-in types can be used to create any of this type:
40
Python Fundamentals: A Functional Approach
>>>s1 = set()
>>>s2 = frozenset()
Remember using a pair of empty curly braces returns a dictionary object instead of a set. Non-empty
sets (not frozensets) can be created by placing a comma-separated list of elements within braces, in
addition to the set constructor as shown below:
Listing 3.12 Examples of valid set creation
>>>s3 = {1,}
>>>s4 = {1}
>>>s5 = {4,4.3}
>>>s6 = set({1,2,3,6.7,'w',(1,2)})
Inside the set and frozeset constructor, items should be of a hashable type. In general, all immutable
types listed in Table 3.6 can be used to create a set or frozenset. A dictionary type can also be used to
create any of these types, only keys are considered as items of the set.
>>>set({3:2})
{3}
>>>frozenset({3:2})
frozenset({3})
The discussion of sets concludes here. The remaining concepts and operations will be covered in
Chapter 7.
3.4.4 User-defined Type
The purpose of a user-defined type is to create a custom data type that meets the specific needs of a
particular application or domain. By defining a new type, you can encapsulate data and its behaviour
into a single unit, which makes your code more organized, modular, and reusable. A user defined type
has specific need and advantages over other data types discussed in previous sections. In python a user-
defined data type can be created using the keyword class. Here's an example of a simple class creation
without any body:
>>> class Sample:
... pass
The indentation is necessary to start with the body of a class and the pass statement in the above
class declaration represents an empty class. This class hasn’t done anything yet. We can create an object
of this class by calling its constructor:
>>> obj = Sample()
>>> obj
<__main__.Sample object at 0x000002043F3DF810>
In the output, <__main__.Sample> indicates that the Sample class is defined in the current Python
module (__main__), signifies that obj is an object at memory address 0x000002043F3DF810. We
mostly interact with objects through methods and attributes, which is discussed in the Second part of
the book.
Python doesn’t have NULL keyword like other computer languages, but there is a None type. It is the
41
Python Fundamentals: A Functional Approach
function’s return value that “doesn’t return anything”. None is often used to represent the absence of a
value, as default parameters are not passed to the function. You can’t assign a NULL value to the
variable; if you do, it is illegal, and it will raise a NameError. Some important points related to None
value are given below:
▪ Comparing None to anything will always return False except None itself.
▪ None is not a 0.
▪ None is not an empty string.
▪ None is not the same as False.
In Python None is a special keyword that signifies the absence of value. It's used to indicate that a
variable doesn't hold any data or that a function doesn't return anything explicitly.
>>> type(var)
<class 'NoneType'>
>>> if var is None:
... var = 3
...
>>> var
3
Python data types discussed in the previous section can be categorized into two groups based on, “how
objects need to be handled during their lifetime”? Such as mutable and immutable and categorization of
each object are listed in Table 3.6.
42
Python Fundamentals: A Functional Approach
3.5.1 Mutability
Mutable in Python can be defined as the object that can change or be regarded as something changeable
in nature. In Python mutable objects means the ability to modify the value of any object. In Listing 3.13,
memory address of the list my_list, is printed using built-in function id(), i.e., 3048260948800. Later
in the next line, 0th index item of my_list is updated to 10 without any error. It means a list object is
mutable by nature and its id also remains same.
Listing 3.13 Mutability testing of list type
3.5.2 Immutability
Immutable objects in Python can be defined as objects that do not change their values and attributes
over time. These objects become permanent once created and initialized. Python has numbers, tuples,
strings, frozen sets, and user-defined classes with some exceptions. Values of these types can’t be
changed and remains permanent once they are initialized and hence called immutable. Once modifying
different memory block is refereed by the name and this can be verified using id(). In Listing 3.14,
immutability has been tested over string object str1, and after setting to new value ‘Python’, its memory
address is changed. Means after modifying str1 start referring to a new memory block.
We have already seen couple of examples of variable assignments, a statement having an equal sign (=).
Variables are symbolic names in any programming and in this section, we will learn various types of
assignments based on data. Like others, in Python, variables need not be declared or defined in advance.
To create a variable, you just assign it a value and then start using it.
>>>a = 4
This statement can be interpreted as “a is assigned the value 4.” Once this is done, a can be used in any
statement or expression, and its value will be substituted, as shown below:
>>>a+4
8
43
Python Fundamentals: A Functional Approach
In the last expression the value of a, is substituted and added with 4 and gets the result as 8. It is
also possible to update the value of a, or can be used to assign to a new variable b, as shown below:
The above statement is an example of multiple variable assignment in a single line. It assigns values to
four variables (a, b, c, and d) in a single statement. Each variable is assigned a corresponding value from
the right-hand side of the statement.
Programs often need to obtain data from the user, usually by way of input from the keyboard. One way
to accomplish this in Python with input() function and its general form is given below:
variable_name = input([prompt])
[prompt] is optional inside the parentheses of the input() function, you can include an optional
44
Python Fundamentals: A Functional Approach
prompt message, which is a string enclosed in a single or double quotes. This prompt provides guidance
or instructions to the user about what kind of input is expected. If you don't specify any prompt, the
input() function will simply wait for the user to type something. Here's an example of how you might
use this syntax:
The print() function can take multiple arguments, separated by commas. It will print each argument,
separated by a space, and automatically add a newline character (\n) at the end by default:
45
Python Fundamentals: A Functional Approach
>>>name = "Alice"
>>>age = 30
>>>print("Name:", name, "Age:", age)
Name: Alice Age: 30
Calling print() without arguments results in a blank line, which is a line comprised solely of the
newline character, e.g. print(‘\n’). Don’t confuse this, with an empty line (means zero characters),
not even the newline e.g. print(‘’). Before passing a string object for printing, it can be formatted in
many ways and all such methods are discussed in Section 5.9 of Chapter 5.
In Python 3, print() function uses two keyword arguments ‘sep’ and ‘end’ to modify the output
string. While printing, print() function inserts a space between the values and that can be modified
by using the keyword argument ‘sep’. See the listed example and in the output of second print(),
comma(,) has been placed after every string literals.
>>> print(12,11,2022,sep='/')
12/11/2022
>>> print(12,11,2022,sep='-')
12-11-2022
Unless the end keyword is used within print, it will always append a newline character (‘\n’), at the end
of what is being printed.
>>> print('A',end='');print('B');print('C')
AB
C
Due to use of end keyword within first print statement, output of the second print, placed in the same
line as ‘AB’. But the third print statement prints C in a newline, because second print statement doesn’t
come with end keyword. Both keywords can be used together, that can help us to print data in a readable
fashion.
The Python interpreter has several functions and types built into it that are always available.
These are listed in with a short description about their work. We would recommend go to the
46
Python Fundamentals: A Functional Approach
link as given in the footnote of this page, to know more about this built-in21 functions.
Table 3.7 List of built-in functions
Function Description
abs(x) Return the absolute value of a number x. The argument may be an
integer, a floating-point number. For complex number, functions
return its magnitude.
aiter(async_iterable) Return an asynchronous iterator for an asynchronous iterable.
all(iterable) Returns True if all the elements of an iterable object are True
any(iterable) Returns True if atleast one of the elements of an interable is True
ascii(object) Return a string in printable from, but escaping the non-ASCII
characters
bin(n) Converts an integer number to its binary from with prefixed 0b.
bool(x=False) Return a Boolean value, i.e. one of True or False.
breakpoint() A built-in tool introduced in Python 3.7 that simplifies debugging by
pausing your code's execution at a specific line.
bytearray() Return a new array of byte.
bytes() Return a new “bytes” object which is an immutable sequence of
integers in the range 0 <= x < 256.
callable(object) Returns True, if the object argument appears callable, False if not.
chr(i) Return the string representing a character whose Unicode code point is
the integer i. For example, chr(97) returns the string 'a'
classmethod(anymethod) In Python 3, it is a decorator (@classmethod) instead of function and
used to define a class method.
compile() Compile the source into a code or AST object.
complex() Returns a complex number
delattr(object, name) Is used to dynamically delete an attribute from an object.
dict() Creates a new dictionary
dir() Without arguments, return the list of names in the current local scope.
dir(object) With an argument, attempt to return a list of valid attributes for that
object.
divmod(a, b) Take two (non-complex) numbers as arguments and return a pair of
numbers consisting of their quotient and remainder when using integer
division.
enumerate(iterable) Return an enumerate object. iterable must be a sequence
eval(expression) The expression argument is parsed and evaluated as a Python
expression
exec(object) This function supports dynamic execution of Python code and object
must be either a string or a code object.
filter(function, Construct an iterator from those elements of iterable for which
iterable) function is true.
21
https://docs.python.org/3/library/functions.html
47
Python Fundamentals: A Functional Approach
48
Python Fundamentals: A Functional Approach
type.
repr(object) Return a string containing a printable representation of an object.
reversed(seq) Return a reverse iterator
round(number, ndigits) Return number rounded to ndigits precision after the decimal point.
set() Return a new set object
setattr(object, name, The function assigns the value to the attribute, provided the object
value) allows it.
slice(stop) Return a slice object representing the set of indices specified by
range(start, stop, step).
sorted(iterable) Return a new sorted list from the items in iterable.
staticmethod() In Python 3, it is a decorator @staticmethod, transform a method into
a static method.
str(object) Return a string version of an object.
sum(iterable) Returns the sum of an iterable
super() Allows you to access and call methods from a parent (super) class
within a subclass (child class).
tuple() Returns a tuple
type(object) Returns the type of an object
vars(object) Return the __dict__ attribute for a module, class, instance, or any
other object with a __dict__ attribute.
zip(*iterables) Iterate over several iterables in parallel, producing tuples with an item
from each one.
__import__() Invoke by import statement.
Key Takeaway’s
▪ Keywords are reserved words in Python, and they are defined for specific purpose.
▪ It is possible to test a string as keyword or not by importing keyword module.
▪ Commenting your code is an important part of documentation and python provides
commenting as single line or multi-line.
▪ In python some data types are comes with built-in and some of them we can create
using class as user defined.
▪ int(), float(), complex(), Decimal(), Fraction() constructor methods are used to from a
respective type.
▪ In Python Integers supports variables length precision, it depends on the available
memory.
▪ Python uses a fixed number of bytes (8 bytes) to represent floats. Therefore, it can
represent some numbers in binary approximately.
▪ It is necessary to import decimal module to create an object of Decimal type and pass
the number with required digits as string.
▪ A factional type can be converted to floating-point, integer, string and complex type
using built-in constructor methods.
49
Python Fundamentals: A Functional Approach
50
4 OPERATORS
In programming, operators are symbols or keywords that represent certain actions to be taken on data.
They are used to perform mathematical, logical, and other operations on values and variables in a
program. Python, as a versatile programming language, provides a wide range of operators for different
types of data and operations. The use of operators is fundamental to programming, and understanding
how to use them correctly is essential for developing efficient and effective code. Python operators come
in various types, such as arithmetic, comparison, logical, bitwise, and more. Each type of operator has
its own set of rules and syntax that govern its usage in a program.
In this chapter, we will explore the different types of operators available in Python and how to use
them. We will also delve into the importance of operators in programming and how they improve the
efficiency and readability of code. Additionally, we will cover practical examples of using operators in
Python and how they can be used in real-world applications.
Python provides a variety of operators, such as arithmetic, comparison, assignment, logical, bitwise,
membership, and identity. Table 4.1 lists the importance of each of them, and later in the subsequent
sections, we have covered the detailed working process with coding examples.
51
Python Fundamentals: A Functional Approach
In Python, there are nine arithmetic operators broadly categorised into two groups, such as unary and
binary depending upon the number of operands required. Unary operators are a special case of
arithmetic operators that operate on a single operand. In Python, there are two unary arithmetic
operators:
▪ Unary plus (+): This operator simply returns the value of its operand, which can be a positive
or negative number. It is rarely used since it does not change the value of its operand.
▪ Unary minus (-): This operator negates the value of its operand, effectively changing the sign of
the number. It can be used to represent negative numbers or to perform operations such as
subtraction by adding the negated value.
1 >>>a = 10
2 >>>b = +a #unary plus
3 >>>print(b)
4 10
5 >>>c = -a #unary minus
6 >>>print(c)
7 -10
In the second line, the unary plus (+) operator is used to assign the value of a to b. Since + is a unary
operator in this case, it simply returns the value of a as it is, without changing its sign. Therefore, the
value of b is also 10. In the fifth line, the unary minus (-) operator is used to assign the negation of
variable a to c. This means that the value of c is -10, but the value of a, remain unchanged.
The use of unary operators is particularly useful when dealing with arithmetic operations on variables.
For example, the unary minus operator can be used to invert the sign of a variable and unary ‘+’ operator
is less commonly used but can be used to enforce positive values in certain calculations.
In Python, there are seven binary arithmetic operators that can be used to perform basic arithmetic
operations on numeric data types such as integers, floating-point numbers, and complex numbers. The
details of each one of them is listed in Table 4.2.
See the example listed in code Listing 4. 1 and suggesting you write the complete code in Python
interpreter to understand the behavior of arithmetic operators.
Listing 4. 1 Sample program of arithmetic operators
>>>a = 10
>>>b = 5
>>>print(a+b) #addition
15
>>>print(a-b) #substraction
5
>>>print(b-a) #substraction
-5
>>>print(a*b) #multipliction
50
>>>print(a/b) #division
2.0
>>>print(a//b) #floor division
2
>>>print(a**b) #exponent
100000
>>>print(a%b) #reminder
0
The result of standard division (a/b) is always a floating-point type, even if the dividend is evenly
divisible by the divisor. See the example given below:
>>>a = 12
>>>b = 3
>>>print(a/b, type(a/b))
4.0 <class 'float'>
Floor division (//) returns the largest integer that is less than or equal to the quotient of the division.
When the result of floor division is positive, the result is as though the fractional part of the quotient is
truncated, leaving only the integer part. When the result is negative, the result is rounded towards
negative infinity, meaning it is rounded down to the next largest (more negative) integer. See the example
given below:
>>>a = -25
>>>b = 10
>>>print(a//b)
-3
In the above example, a//b returns -3, as -2.5 rounds down to -3 as more negative. The programming
examples 4.1 to 4.3 will add clearer idea to it.
53
Python Fundamentals: A Functional Approach
Program 4.1: Write a program to take two integers as input and using arithmetic operator swap their
values without using third variable.
Program 4.2: A program to calculate the Semester Grade Point Average in a 10 point scale by consider
the following steps:
▪ The number of subjects is 3 with credits 2, 3, and 3.
▪ Take input of the grade of three subjects that you have scored out of a 10 point scale.
▪ Multiply the grade of each subject with its credit and add them.
▪ Add the credit of each subject.
▪ Finally, divide the total score by the total credits to get the Semester Grade Point Average.
54
Python Fundamentals: A Functional Approach
Program 4.3: Write a program that takes the principal amount, rate of interest, and tenure of the loan
in months as input from the user, and calculates the simple interest and total amount to pay.
Program 4.4: Write a program to calculate the compound interest on given principal amount (P), rate
𝑟 𝑛
of interest (R) and time (T). The compound interest formula, 𝐶. 𝐼 = 𝑃 (1 + 100) − 𝑃, can be used,
where 𝑛 represents number of times interest is compounded in a year. Calculate the amount to be repaid
on a loan of Rs. 12000 for one and half years at 10% per annum compounded half yearly?
p = int(input('Enter Principla Amount:'))
r = float(input('Enter rate of interest:'))
t = int(input('Enter time in months:'))
#duration of compunding interest
com_in_months =int(input('Enter Compunding period:'))
#re-calculating roi based on compounding time
roi_in_com_in_months = com_in_months/12 * r
n = t/com_in_months
55
Python Fundamentals: A Functional Approach
Program 4.5: Write a program to take input an integer 𝑛 and calculate the expression 𝑛 + 𝑛2 + 𝑛3 .
1 n = int(input("Enter a number: "))
2 result = n + n**2 + n**3
3 print("Result: ", result)
To calculate the result of the expression 𝑛 + 𝑛2 + 𝑛3 the exponent operator ** is used to raise 𝑛 to
the power of 2 and 3 respectively at line 2, and add the results together. In Python, besides the numeric
data types, arithmetic operators can also be used with certain other object types, listed in Table 4.3:
Table 4.3 Use of arithmetic operators with other object types
Operators Description
equal to (= =) Returns True if the two values being compared are equal, and False otherwise.
not equal to Returns True if the two values being compared are not equal, and False otherwise.
(! =)
greater than Returns True if the first value is greater than the second value, and False otherwise.
(>)
less than (<) Returns True if the first value is less than the second value, and False otherwise.
greater than or Returns True if the first value is greater than or equal to the second value, and
equal (>=) False otherwise.
56
Python Fundamentals: A Functional Approach
less than or Returns True if the first value is less than or equal to the second value, and False
equal (<=) otherwise.
>>>a = 10
>>>b = 5
>>>print(a > b) #True relation
>>>print(a < b) #False relation
>>>print(a >= b) #True relation
>>>print(a <= b) #False relation
>>>print(a == b) #False relation
>>>print(a != b) #True relation
In this program, two variables a, b are initialized with the values 10 and 5, respectively. Each of the
comparison operators (>, <, >=, <=, ==, !=) are used to compare the values of a and b and print
out the result either as True or False. The output of the program will be as follows:
True
False
True
False
False
True
These set of operators always returns the relation between two operands either as True or False of
bool type. See the next illustration which has verified this:
>>>print(type(10>12))
<class 'bool'>
Note: When comparing floating-point values in Python for equality, it's important to keep in mind that
due to the way computers represent floating-point numbers, small rounding errors can occur. As a result,
two floating-point values that should be equal may not be exactly equal when compared using the ==
operator. See the example given below:
>>>x = 1.1
>>>y = 2.2
>>>print((x+y)==3.3)
False
Yikes! The internal representations of the constant 3.3 may not exactly equal to result of, 1.1 + 2.2,
so you cannot rely on the value of the expression to compare with exactly to 3.3.
The preferred way to determine whether two floating-point values are “equal”, we need to consider
this small rounding errors. One common approach is to use a tolerance or "epsilon" value, which is a
small value that represents the maximum allowable difference between the two values for them to still
be considered equal. Consider the example in Listing 4.3, demonstrates how to compare two floating-
point values for equality in Python, using an epsilon value:
57
Python Fundamentals: A Functional Approach
1 a = 0.1 + 0.2
2 b = 0.3
3 epsilon = 1e-10 # set a small epsilon value
4
5 # compare a and b for equality using epsilon
6 if abs(a - b) < epsilon:
7 print("a and b are equal")
8 else:
9 print("a and b are not equal")
To compare a and b for equality, we have subtracted b from a and take the absolute value of the
difference using the abs() function, and compare it to the epsilon value using the < operator. If the
absolute difference between a and b is less than the epsilon value, we consider them as equal.
Note: The choice of epsilon value depends on the specific application and the precision required. A
smaller epsilon value provides greater precision but may also lead to false negatives, while a larger epsilon
value provides less precision but may also reduce the likelihood of false negatives.
In Python, comparison operators can also be used with non-numeric object types such as strings,
lists, tuples, sets, and dictionaries. The comparison is done based on the lexicographical ordering of the
elements in the objects. For example, when comparing strings, Python compares the ASCII values of
each character in the strings. When comparing lists, Python compares the elements in the lists from left
to right, one by one. Similarly, for tuples, sets, and dictionaries, the comparison is done based on the
elements contained in them.
The importance of logical operators lies in their ability to simplify complex decision-making processes
in programs. By combining multiple conditions, programmers can write more concise and readable code
that is easier to understand and maintain. Logical operators also play a crucial role in control flow
statements, such as loops and conditional statements (discussed in Chapter 6), by allowing programs to
perform different actions based on specific conditions.
Like other programming languages Python also offers three logical operators, such as, and, or and
not operators. Out of these logical and, or operators takes two operands and logical not takes one
operand. A simple logical and, or expression can be formed by including any object type, as follows:
operand1 and operand2
operand1 or operand2
In general, expression with comparison operators is considered as operands for logical operators.
Like comparison operator’s logical operators also evaluate every expression and return the outcome as
Boolean True or False. We have summarized the outcome of logical and, or operators in Table 4.5.
The True and False values of Table 4.5 are the outcomes of any comparison expression or values of
any Python object type that can be treated as True and False values. The complete working process of
logical and, or operators are explained next with suitable coding examples.
1 a, b = 10, 20
2 operand1 = a == b
3 operand2 = a > b
4 print(f'{operand1} and {operand2} => {operand1 and operand2}')
5 print(f'{operand1} or {operand2} => {operand1 or operand2}')
In the above code, both the comparison expression at line 2, 3 evaluates to False and according the
Table 4.5 the result of logical and, or operation with both the operands as False conditions will also be
False, as it is verified from the output as well. Let’s see the second case in the next example:
1 a, b = 10, 20
2 operand1 = a != b
3 operand2 = a > b
4 print(f'{operand1} and {operand2} => {operand1 and operand2}')
5 print(f'{operand1} or {operand2} => {operand1 or operand2}')
59
Python Fundamentals: A Functional Approach
1 a, b = 10, 20
2 operand1 = a != b
3 operand2 = a < b
4 print(f'{operand1} and {operand2} => {operand1 and operand2}')
5 print(f'{operand1} or {operand2} => {operand1 or operand2}')
If both the operands of logical and, or operators are True then, final output of operands are also
True, as it can be verified from the output of above code. For a programmer, it is important to
understand the evaluation order of the operands used with logical and, or operators. In Python, logical
and, or operators are evaluated from left to right, as follows:
▪ When using the logical and operator, if the first operand evaluates to False, the second operand
is not evaluated because the expression is already known to be False.
▪ When using the logical or operator, if the first operand evaluates to True, the second operand
is not evaluated because the expression is already known to be True.
The logical not operator in Python is represented by the "not" keyword. It takes a single operand and
returns the opposite Boolean value. It returns the inverted truth value for the operand and its general
syntax is as follows:
not operand
1 x, y = 10, 5
2
3 # Evaluates to False, because x is greater than y
4 result1 = x < y
5 # Evaluates to True, because result1 is False
6 result2 = not result1
7 print(result1) # False
8 print(result2) # True
The values of x and y is compared using < operator, which returns a Boolean value of False because x
is greater than y. Then apply the logical not operator to this result using the "not" keyword, which
returns the opposite Boolean value of True. So, the result of result2 is True, because it is the
opposite of the original comparison result.
4.3.1 How does Python interpreter consider any value as truthy or falsy?
In Python, any value can be evaluated as either True or False, depending on whether it represents a
"truthy" or "falsy" value. The truth value of an object in Python is determined by its Boolean context,
which means the context in which the object is used in a Boolean expression. In general, the following
values are considered "falsy" in Python: All the logical operators work with the truth value of their
operands – but what exactly is a truth value?
We know that the bool type True represents True and False represents False. Python considers zero
to be False and all other numbers, irrespective if they are positive or negative, are considered to be True.
60
Python Fundamentals: A Functional Approach
>>> print(bool(8))
True
>>> print(bool(0))
False
>>> print(bool(-8))
True
In the given example we have checked what type of values are considered as True and False. We can
see the output that 8 and -8 considered as True and zero (0) considered as False. In python most of the
in-built types considered as False:
Operations and built-in functions that have a Boolean result always return 0 or False for false and 1 or
True for true, unless otherwise stated. (Important exception: the Boolean operations or, and always
return one of their operands.)
Logical and, or operators in Python are short-circuited which means they evaluate only the bare
minimum required to get the correct result. For example:
If you’re coming from another programming language such as Java, then you’ll immediately notice
that Python is missing the unsigned right shift operator denoted by three greater-than signs (>>>). This
has to do with how Python represents integers internally. Since integers in Python can have an infinite
number of bits, the sign bit doesn’t have a fixed position. In fact, there’s no sign bit at all in Python.
All binary bitwise operators have a corresponding compound operator that performs an augmented
assignment, as listed in Table 4.7:
The resulting numbers uniquely identify the text characters within the Unicode space, but they’re
shown in decimal form. To get binary version of any number we can use the bin().
Variable bit-lengths are problematic. If you were to put those binary numbers next to one another
on an optical disc, for example, then you’d end up with a long stream of bits without clear boundaries
63
Python Fundamentals: A Functional Approach
The resulting bit pattern is an intersection of the operator’s arguments. It has two bits turned on in
the positions where both operands are ones. In all other places, at least one of the inputs has a zero bit.
Arithmetically, this is equivalent to a product of two bit values. You can calculate the bitwise AND of
numbers a, b by multiplying their bits at every index i:
(𝑎 & 𝑏)𝑖 = 𝑎𝑖 × 𝑏𝑖
A bit having 1 multiplied by 1 gives 1, but anything multiplied by zero will always result in zero.
Alternatively, you can take the minimum of the two bits in each pair. Notice that when operands have
unequal bit-lengths, the shorter one is automatically padded with zeros to the left. See the coding
example, given next.
>>> a, b = 16, 8
>>> c = a & b
>>> print(f'{bin(a)} & {bin(b)} = {bin(c)}')
0b10000 & 0b1000 = 0b0
>>> print(f'{a} & {b} = {c}')
16 & 8 = 0
In the above example, we have first printed the output in binary and then in decimal form. At first
both numbers will be represented to same bit length as 8 bits. Because decimal 16 and 8 requires 5 and
4 bits respectively. During processing CPU will read byte by bytes, both the numbers need to be
represented in 8 bits form and then bitwise AND will be applied over each pair of bits, as shown below:
0 0 0 1 0 0 0 0
& 0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 0
4.4.4 Bitwise OR
64
Python Fundamentals: A Functional Approach
The bitwise OR operator (|) performs logical disjunction. For each corresponding pair of bits, it returns
1, if at least one of the bit is switched on. The resulting bit pattern is a union of the operator’s arguments.
It turned on a bit where either of the operands has a bit as 1. Only a combination of two zeros gives a
zero in the final output. See the process listed in Table 4.9.
Table 4.9 Working Process of Bitwise OR Operator
The arithmetic behind it is a combination of a sum and a product of the bit values. To calculate the
bitwise OR of numbers a, b, you need to apply the following formula to their bits at every index i,
and see the working of bitwise OR, in the given coding example.
(𝑏)𝑖 = 𝑎𝑖 + 𝑏𝑖 − (𝑎𝑖 × 𝑏𝑖 )
>>> a, b = 16, 8
>>> c = a | b
>>> print(f'{bin(a)} | {bin(b)} = {bin(c)}')
0b10000 | 0b1000 = 0b11000
>>> print(f'{a} | {b} = {c}')
16 | 8 = 24
At first both numbers will be represented to same bit length as 8 bits. Then bitwise OR will be applied
over each pair of bits, as shown below:
0 0 0 1 0 0 0 0
| 0 0 0 0 1 0 0 0
0 0 0 1 1 0 0 0
The name XOR stands for “exclusive or” since it performs exclusive disjunction on the bit pairs. In
other words, every bit pair must contain opposing bit values to produce a one:
65
Python Fundamentals: A Functional Approach
Visually, it’s a symmetric difference of the operator’s arguments. In Table 4.10, three bits switched
on in the result where both numbers have different bit values. Bits in the remaining positions cancel out
because they’re the same.
Similarly to the bitwise OR operator, the arithmetic of XOR involves a sum. However, while the
bitwise OR clamps values at one, the XOR operator wraps them around with a sum modulo two:
Modulo is a function of two numbers—the dividend and the divisor—that performs a division and
returns its remainder. In Python, there’s a built-in modulo operator denoted with the percent sign (%).
>>> a, b = 34, 12
>>> c = a ^ b
>>> print(f'{bin(a)} ^ {bin(b)} = {bin(c)}')
0b100010 ^ 0b1100 = 0b101110
>>> print(f'{a} ^ {b} = {c}')
34 ^ 12 = 46
At first both numbers will be represented to same bit length as 8 bits. Then bitwise XOR applied
over each pair of bits, as shown below:
0 0 1 0 0 0 1 0
^ 0 0 0 0 1 1 0 0
0 0 1 0 1 1 1 0
The last bitwise logical operator is the bitwise NOT operator (~), which takes only one operand, making
it the only unary bitwise operator. It performs logical negation on a given number by flipping all of its
bits:
~ 0 0 1 0 0 0 1 0
1 1 0 1 1 1 0 1
The inverted bits are a complement to one, which turns zeros into ones and ones into zeros. It can
be expressed arithmetically as the subtraction of individual bit values from one:
~𝑎𝑖 = 1 − 𝑎𝑖
While the bitwise NOT operator seems to be the most straightforward of them all, you need to
exercise extreme caution when using it in Python. Everything you’ve read so far assumes that numbers
are represented with unsigned integers.
Note: Unsigned data types don’t let you store negative numbers such as -273 because there’s no space
for a sign in a regular bit pattern. Trying to do so would result in a compilation error, a runtime exception,
or an integer overflow depending on the language used.
Although there are ways to simulate unsigned integers, Python doesn’t support them natively. That
66
Python Fundamentals: A Functional Approach
means all numbers have an implicit sign attached to them whether you specify one or not. This shows
when you do a bitwise NOT of any number:
>>>a = 156
>>>bin(a)
'0b10011100'
Now if we flip all the bits of a using bitwise NOT, the bits of a will be something like ‘0b01100011’,
which is 9910 in decimal, but Python tells different story:
>>>~a
-157
Remember, one’s complement of any positive number will give a negative number, because of change
in sign bit. In a computer system, the left most bit is reserved for preserving the sign of any number. If
the sign is ‘+’ then left most bit will be 0, else 1 for negative numbers. To store any negative value
computer systems uses two’s complete of the number and follows the steps given below:
▪ Extracts all the bits except the sign bit from the resultant binary string, that we received after
applying one’s complement. So, we have ‘0b01100011’, we didn’t consider the sign bit, which
is already 1.
▪ In the next step, we need to find the one’s complement of ‘0b01100011’, which will be
‘0b10011100’.
▪ In the last step, we will perform binary 1 addition with the binary number received in previous
step, which will be ‘0b10011101’means 15710.
▪ In the complete process the sign bit remains unchanged and the output is produced after
converting the binary string to decimal with a prefixed negative sign.
>>>print(0b10011101)
157
Bitwise shift operators are another kind of tool for bit manipulation. They let you move the bits around,
which will be handy for creating bitmasks later. In the past, they were often used to improve the speed
of certain mathematical operations.
Left Shift
The bitwise left shift operator (<<) moves the bits of its first operand to the left by the number of places
specified in its second operand. It also takes care of inserting enough zero bits to fill the gap that arises
on the right edge of the new bit pattern. Let’s see an example,
67
Python Fundamentals: A Functional Approach
>>> a = 12
>>> a << 1
24
In the above code we are shifting the bits of a by 1 position towards left and the value 12 will be
multiplied by 2 and 24 will be saved into b. So, the print statement will print 24. Let’s see a pictorial view
of the above example:
Bit position 7 6 5 4 3 2 1 0
Operand 1 0 0 0 0 1 1 0 0
After left shift 0 0 0 1 1 0 0 0
Now the second operand holds the value 1. So, every bits will change their position by 1 towards
left. So the new number after shifting of 1 bit is 11000 and the decimal of this number is 24. Shifting a
single bit to the left by one place doubles its value. Moving bits in two places to the left will quadruple
the resulting value. In general, shifting bits to the left corresponds to multiplying the number by a power
of two, with an exponent equal to the number of places shifted:
𝑎 ≪ 𝑛 = 𝑎 × 2𝑛
The left shift used to be a popular optimization technique because bit shifting is a single instruction
and is cheaper to calculate than exponent or product. Today, however, compilers and interpreters,
including Python’s, are quite capable of optimizing your code behind the scenes.
Right Shift
The bitwise right shift operator (>>) is analogous to the left one, but instead of moving bits to the left,
it pushes them to the right by the specified number of places. The rightmost bits always get dropped.
Let’s take an example:
>>> a = 39
>>> bin(a)
'0b100111'
>>> b = a >> 1
>>> bin(b)
'0b10011'
The above code demonstrates the bitwise right shift (>>) operation. Unlike arithmetic division, a
right shift by 1 position on a binary number is equivalent to dividing by 2, its decimal value. The code
assigns the shifted value to b. However, it's important to note that the right shift operation discards the
least significant bits (those on the right). This effectively truncates the division result, discarding any
fractional component.
Bit position 7 6 5 4 3 2 1 0
Operand1 0 0 1 0 0 1 1 1
after right shift 0 0 0 1 0 0 1 1
68
Python Fundamentals: A Functional Approach
Every time you shift a bit to the right by one position, you halve its underlying value. Moving the
same bit by two places to the right produces a quarter of the original value, and so on. When you add
up all the individual bits, you’ll see that the same rule applies to the number they represent:
Halving an odd number such as 157 would produce a fraction. To get rid of it, the right shift operator
automatically floors the result. It’s virtually the same as a floor division by a power of two:
𝑎
𝑎 ≫ 𝑛 = ⌊ 𝑛⌋
2
The bitwise right shift operator and the floor division operator both work the same way, even for
negative numbers. However, the floor division lets you choose any divisor and not just a power of two.
Using the bitwise right shift was a common way of improving the performance of some arithmetic
divisions. You might be wondering what happens when you run out of bits to shift. For example, when
you try pushing by more places than there are bits in a number:
>>> a = 3
>>> a >> 5
0
In this example a, is right shifted by 5 bits. But the binary of 3 is 112, which has two bits only. So in
this case result will be always zero. Once there are no more bits switched on, you’re stuck with a value
of zero. Zero divided by anything will always return zero. However, things get trickier when you right
shift a negative number because the implicit sign bit gets in the way:
>>> a = -2
>>> a >> 5
-1
The rule of thumb is that, regardless of the sign, the result will be the same as a floor division by
some power of two. The floor of a small negative fraction is always minus one, and that’s what you’ll
get.
You have seen that a single equal sign (=) is used to assign a value to a variable. It is, of course, perfectly
viable for the value to the right of the assignment to be an expression containing other variables:
69
Python Fundamentals: A Functional Approach
>>> a = 10
>>> b = 20
>>> c = a * 5 + b
>>> c
70
In fact, the expression to the right of the assignment can include references to the variable that is being
assigned to:
>>> a = 10
>>> a = a + 5
>>> a
15
>>> b = 20
>>> b = b * 3
>>> b
60
The first example is interpreted as “a is assigned the current value of a plus 5,” effectively increasing
the value of a by 5. The second reads “b is assigned the current value of b times 3,” effectively increasing
the value of b threefold. Of course, this sort of assignment only makes sense if the variable in expression
a + 5 or b * 3 has already previously been assigned a value:
Table 4.11 List of Arithmetic and Bitwise Operators Supports shorthand assignment
Arithmetic Bitwise
+ &
- |
* ^
/ >>
% <<
//
**
70
Python Fundamentals: A Functional Approach
For the above listed operators following two expressions are equivalent:
In the above syntax “operator” represent any of the operators from arithmetic and bitwise listed in
Table 4.11. Look at the next example, where we have highlighted three types of assignment statement
and produce same result for x and y.
Python offers two membership operators to check or validate the membership of a value. It tests for
membership in a sequence, like a list, tuple, set or string.
The ‘in’ operator is used to check if a character/ substring/ element exists in a sequence or not. Evaluate
to True if it finds the specified element in a sequence otherwise, False. For example,
>>> l = [2, 3, 4]
>>> 2 in l
True
>>> 10 in l
False
>>> s = (2, 3)
>>> 2 in s
True
>>> 4 in s
False
The ‘not in’ operator is the opposite of ‘in’. It returns True, if the specified value is not found in the
sequence. Otherwise, it returns False.
71
Python Fundamentals: A Functional Approach
Identity operators compare the memory locations of two objects. There are two Identity operators
explained below:
is not Evaluates to false if the variables on either side of the x is not y, here is not results
operator point to the same object and true otherwise. in 1 if id(x) is not equal to id(y).
In python every object has an identity, a type and a value. An object’s identity never changes once it
has been created; you may think of it as the object’s address in memory. The ‘is’ operator compares the
identity of two objects; the id() function returns an integer representing its identity.
>>> a = 20
>>> b = 20
>>> a is b
True
>>> id(a), id(b)
(140715626501512, 140715626501512)
This code snippet checks if two variables, a and b, refer to the same object in memory and not just
having the same value. The line a is b, uses the is operator, checks if a and b refer to the exact same
object in memory. In this case, since both variables are assigned the same small integer value (20), Python
might optimize by storing only one copy of the value and making both a and b point to that single copy.
Both a and b refer to the same memory block as confirmed using id().
For small integers like 20, Python might have an internal pool of pre-allocated memory for these
values to improve performance. Assigning the same value to multiple variables allows Python to simply
reference the existing object instead of creating a new one in memory for each variable. This behavior
depends on the data type, value, and potentially even Python implementation details. Here are some
scenarios where a is b, might return False:
72
Python Fundamentals: A Functional Approach
▪ Larger integers or other data types: For larger integers or other data types like strings or lists,
Python might not pre-allocate memory and would likely create separate objects for each variable
assignment.
▪ Explicit object creation: If you explicitly create objects using functions or constructors (a =
list(range(5)), b = list(range(5))), even if they have the same content, they will be
separate objects.
In the next example we are demonstrating that two objects will refer different memory address.
>>> a = 20.5
>>> b = 20
>>> a is b
False
>>> id(a), id(b)
(2424282604688, 140716333962632)
In the above example, a and b are two different type objects, a as float and b as int. The identity
operator return False, as a and b, referring to different memory blocks. It is also true for large integers.
Let us try is operator with mutable data types such as list.
73
Python Fundamentals: A Functional Approach
>>> l1 = [1,2,3]
>>> l2 = [1,2,3]
>>> l3 = l1
>>> l1 is not l2
True
>>> l1 is not l3
False
The behavior of is and is not operator also depends on the type of object. Python has two types
of objects (i) mutable and (ii) immutable. Details of the mutable and immutable types are discussed in
Chapter 3. Types affect almost all aspects of object behavior. Even the importance of object identity is
affected in some sense: for immutable types, operations that compute new values may return a reference
to any existing object with the same type and value, while for mutable objects this is not allowed.
For example, a = 1 and b = 1, a and b may or may not refer to the same object with the value one,
depending on the implementation of python, but after c = []; d = [], c and d are guaranteed to refer
to two different, unique, newly created empty lists.
While dealing with operators in Python, we must know about the concept of Python operator
precedence and associativity as these determine the priorities of the operator otherwise, we’ll see
unexpected outputs.
Imagine an expression with multiple operators like addition, multiplication, and division. Precedence
determines which operations are evaluated first. Operators with higher precedence are evaluated before
those with lower precedence. This ensures the expression is evaluated in the intended order. Check the
example given below:
>>> a = 4 + 5 * 3
>>> a
19
Here multiplication operator is higher in precedence. So, 5 * 3 will be evaluated first and 15 will be
added with 4. We can also change the order of evaluation using a pair of brackets, as shown below:
>>> a = 4 / (5 + 3)
>>> a
0.5
In the above code addition is preformed over division, because the expression 5+3 surrounded by a
pair of parentheses. Here is the order of precedence of the Python operators you have seen so far, from
lowest to highest:
74
Python Fundamentals: A Functional Approach
Operators at the top of the Table 4.13 have the highest precedence, and those at the bottom of the
table have the lowest. Any operators in the same row of the table have equal precedence. That’s why
multiplication is performed first, as it is higher precedence than addition.
Similarly, in the next example, 3 raised to the power of 4, will be evaluated first, and then
multiplications are carried out in order from left to right (2 * 81 * 5 = 810):
>>> 2 * 3 ** 4 * 5
810
Operator precedence can be overridden using parentheses. Expressions in parentheses are always
performed first, before expressions that are not parenthesized. Thus, the following happens:
>>> 20 + 4 * 10
60
>>> (20 + 4) * 10
240
The two expressions generate different results because of operator precedence in Python. Operator
precedence defines the order in which operations are evaluated within an expression. In the first
expression, multiplication (*) has higher precedence than addition (+). Therefore, 4 * 10 is evaluated
first (40). Then, 40 is added to 20, resulting in 60. In the second expression parentheses () force the
evaluation of 20 + 4 first (24), then 24 is multiplied by 10, resulting in 240.
There is nothing wrong with making liberal use of parentheses, even when they aren’t necessary to
change the order of evaluation. In fact, it is considered good practice, because it can make the code
more readable, and it relieves the reader of having to recall operator precedence from memory. Consider
the following:
75
Python Fundamentals: A Functional Approach
>>> a, b = 30, 10
>>> (a < 40) and (b > 5)
True
Use of parentheses is fully unnecessary, as the comparison operators have higher precedence than
logical and does and would have been performed first anyhow. But some might consider the intent
of the parenthesized version more immediately obvious than this version without parentheses:
If an expression contains two or more operators with the same precedence, then operator associativity
is used to evaluate the expression. It can either be Left to Right or from Right to Left. Next example
shows multiple operators with same precedence. To solve this expression, we have to understand how
operator associativity works. Details order of operator associativity is listed in Table 4.13. Some of the
coding examples are listed next to understand the working of operator associativity.
>>> 4 / 5 * 3
2.4000000000000004
In the above expression division operation is evaluated first, which results 0.8 and multiplied with 3.
Due to the rule of associativity this expression is evaluated from left to right. Associativity of operators
are considered when in a given expression more than one operator are used with same precedence. The
right to left associativity can be seen in the next example.
>>> 2 ** 3 ** 2
512
In the above expression, 3 ** 2 will be evaluated first, which results in 9 followed by 2 ** 9 is calculated,
using right to left associativity. In python, all the unary (operates over single operand) operators followed
right to left associativity, as listed in Table 4.13.
In Python, non-associative operators are operators where the order of evaluation within an expression
doesn't follow the left-to-right or right-to-left rule that applies to most operators. This means you cannot
chain them together and expect a well-defined result. Here are the key points about non-associative
operators in Python:
76
Python Fundamentals: A Functional Approach
▪ No defined order of evaluation: Unlike operators with precedence (like addition and
multiplication), non-associative operators have no set order in which they are evaluated. This
can lead to unexpected results if you try to chain them together.
▪ Limited examples: There are only a few non-associative operators in Python, primarily
assignment operators and some comparison operators used in a specific way.
Operators like =, +=, -=, etc., are non-associative. You cannot combine them like x = y = z += 1. This
would result in a syntax error.
a = b = c += 1
File "<stdin>", line 1
a = b = c += 1
^^
SyntaxError: invalid syntax
While comparison operators (<, >, <=, >=, ==, !=) themselves are associative (evaluated left-to-right),
using them in a chain without parentheses is non-associative. For example, a < b < c is not equivalent
to (a < b) and (b < c). It's generally discouraged to chain.
Key Takeaway’s
▪ Logical operators used to combine conditional statements: and, or, and not.
▪ Membership Operators are used to test if a sequence is presented in an object: in, not in.
▪ Identity Operators are used to compare the memory locations of two objects: is, is not.
▪ Operators with higher precedence are evaluated before operators with lower precedence.
▪ When operators have the same precedence, associativity determines the order of
evaluation. Most operators in Python have left-to-right associativity, meaning they are
evaluated from left to right.
▪ Use parentheses () to override the default precedence and associativity.
▪ The exponentiation operator ** has right-to-left associativity, which is an exception to the
common left-to-right rule.
▪ Unary plus + and minus - (as in +x, -x) have higher precedence than the standard arithmetic
operators.
▪ Multiplicative operators (*, /, //, %) have higher precedence than additive operators
(+, -).
▪ All comparison operators have the same precedence, which is lower than that of arithmetic
and bitwise operators.
▪ The logical not has higher precedence than and, which has higher precedence than or
****************END OF CHAPTER****************
77
5 STRINGS
In Python a string is a sequence of Unicode code points. These code points are converted into a
sequence of bytes for efficient storage. This process is called character encoding. There are many
encodings such as UTF-8, UTF-16, ASCII etc. and Python uses UTF-8 encoding by default.
ASCII can only represent a limited number of characters at one time. It isn't very useful to represent
any language that isn't based on a Latin character set. ASCII only defines 128 characters. So only 7 bits
is normally used in ASCII which is not sufficient to support characters that required more bits to
represent. At present every computer program must be scalable enough to take care various international
characters and an application needs to be internationalized to display the same output or warning
message in a variety of languages in English, French, Japanese, Hebrew, or Russian. Web content can
be written in any of these languages and can also include a variety of emoji symbols. Python’s string type
uses the Unicode Standard for representing characters, which lets Python programs work with all these
different possible characters.
However, UTF-8 which is an encoding standard for UCS-4 (Unicode) can represent almost any
language. It does this by chaining multiple bytes together to represent one character (or glyph to be
more correct). A character is represented on a screen or on paper by a set of graphical elements that’s
called a glyph. The glyph for an uppercase A, for example, is two diagonal strokes and a horizontal
stroke, though the exact details will depend on the font being used. Most Python code doesn’t need to
worry about glyphs; figuring out the correct glyph to display is generally the job of a GUI toolkit or a
terminal’s font renderer.
5.1.1 Encoding
Unicode string is a sequence of code points, which are numbers from 0 through 0x10FFFF (1,114,111
decimal). This sequence of code points needs to be represented in memory as a set of code units, and
code units are then mapped to 8-bit bytes. The rules for translating a Unicode string into a sequence of
bytes are called a character encoding, or just an encoding.
The first encoding you might think of is using 32-bit integers as the code unit, and then using the
CPU’s representation of 32-bit integers. In this representation, the string “Python” might look like this:
78
Python Fundamentals: A Functional Approach
This representation is straightforward but using it presents a number of problems. Such as:
Therefore, this encoding isn’t used very much, and people instead choose other encodings that are more
efficient and convenient, such as UTF-8. UTF-8 is one of the most used encodings, and Python often
defaults to using it. UTF stands for “Unicode Transformation Format”, and the
‘8’ means that 8-bit values are used in the encoding. (There are also UTF-16 and UTF-32 encodings, but
they are less frequently used than UTF-8.) UTF-8 uses the following rules:
▪ If the code point is < 128, it’s represented by the corresponding byte value.
▪ If the code point is >= 128, it’s turned into a sequence of two, three, or four bytes, where each
byte of the sequence is between 128 and 255.
A string literal is a sequence of characters enclosed in quotes that represents text data in a programming
language. It's a fixed value that cannot be changed during the program's execution. String literals are
used for various purposes, such as displaying messages, storing text data, and performing string
79
Python Fundamentals: A Functional Approach
manipulations. Python offers several ways to create string literals, providing flexibility for different use
cases.
String literals in python are surrounded by either single quotation (‘’) marks, or double quotation
marks (“ ”). In python single and double-quoted strings are the same and return the same type of
object. This type of objects supports only single line string only, as shown below:
This is a multiline
string surrounded by single quotes
This is a multiline
string surrounded by double quotes
A raw string is a type of string that can also include one or many escape characters. A raw string is
formed by prefixing an 'r' or 'R', a character following a backslash is included in the string without
change, and all backslashes are left in the string. For example, the string literal r"\n" consists of two
characters: a backslash and a lowercase 'n'.
>>> s = r'\n'
>>> print(s)
\n
80
Python Fundamentals: A Functional Approach
String quotes can be escaped with a backslash, but the backslash remains in the string; for example,
r"\"" is a valid string literal consisting of two characters: a backslash and a double quote.
>>> s = r"\""
>>> print(s)
\"
Any raw string contains only one backslash, r"\" is not a valid string literal (even a raw string cannot
end in an odd number of backslashes). Specifically, a raw string cannot end in a single backslash (since
the backslash would escape the following quote character). Note also that a single backslash followed
by a newline is interpreted as those two characters as part of the string, not as a line continuation.
>>> s = r'\
... Hello'
>>> s
'\\\nHello'
Escape characters are used to solve the problem of using special characters inside a string declaration.
It directs the interpreter to take suitable actions mapped to that character. We denote escape characters
with a backslash (\) at the beginning. For example, to put a new line in the string we can add a linefeed
(\𝑛). Table 5.1, provides information about all the escape characters supported by Python.
Table 5.1 List of Escape Characters and their purpose
To add a horizontal tab space between two words ‘\𝑡’ need to be inserted between two characters, as
follows:
81
Python Fundamentals: A Functional Approach
In a nutshell, you can’t. Strings are one of the immutable types in Python, meaning not able to be
changed. Trying to modify a part of string through indexing (discussed later in this chapter) will generate
a TypeError.
>>str1 = 'Python'
>>str1[0] = 'p'
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
str1[0] = 'p'
TypeError: 'str' object does not support item assignment
In general, python offers many ways to modify a string. You can usually easily accomplish what you
want by generating a copy of the original string that has the desired change in place.
In the above code at ‘p’+str1[1:], str1 has been modified by slicing and concatenation operation
(+). Using slicing (str1[1:]), we can access any subpart of a string, by proving its boundary. There
is also a built-in string method to accomplish this:
82
Python Fundamentals: A Functional Approach
You have already seen the use of plus (+) and multiplication (*) operators with numeric operands in
Chapter 4. These two operators can be applied to strings as well. The plus ‘+’ operator concatenates
two strings.
>>S1 = 'Python'
>>S2 = ' Language'
>>S1 + S2
'Python Language'
>>'This is String in ' + S1 + S2
'This is String in Python Language'
Two or more strings can also concatenate without using '+' operator, as follows:
>>> 'abc'*3
'abcabcabc'
>>> 3*'abc'
'abcabcabc'
The multiplier operand n must be an integer and if the multiplier is zero or any negative number then
result will be an empty string:
>>> s = 'Hello'
>>> s * 0
''
>>> s * -2
''
5.4 Comparing Strings
Many operators are used in performing the comparison of strings, such as equality operator ( ==), and
comparison operators like (<, >, <=, >=, !=). Strings are compared, by comparing characters of
each string at same position. It will check the character with a lower Unicode and is considered a smaller
value character or larger value character, or equal value character. In Python, strings are compared by
the comparing of ordinal value of characters. Python uses objects with the same values in memory which
makes comparing objects faster. Some basic comparison operators are equal to (==) and ‘is’
operators.
Next some examples are illustrated to understand the string comparison and while comparing
following points need to be consider:
▪ Comparisons are case sensitive, meaning 'G' is not the same as 'g'.
83
Python Fundamentals: A Functional Approach
▪ Each character in a string has an ordinal value, which is what operators look out for, and not
the actual character. For example, ‘G’ and ‘g’ have an ordinal value 71 and 103 respectively.
So, when compared ‘g’ becomes greater than ‘G’.
The equal comparison ‘==’ operator returns True, if two strings are equal, else return False:
>>'Python' == 'Python'
True
>>'Python' == 'python'
False
The Boolean value True returned because both the operands of ‘==’ are same and in the second
comparison, False, return, because ‘P’ and ‘p’ are not same. Not Equal (! =) operator is opposite of
equal (==) operator and checks for non-equal string objects:
Note: Remember that using = would make the interpreter assume you want to assign one value to
another, make sure you are using == operator for comparison.
The less than (<), operator compares the string based on their ordinal values of each character. The
less than (<) and other comparison operators requires two operands in the form str1<str2.
>>> string1 = "Hello"
>>> string2 = "hello"
>>> if string1 < string2:
... print("string1 is less than string2")
... else:
... print("string2 is less than string1")
...
string1 is less than string2
Consider the outcome of the code snippet, which states that string1 is less than string2. This
comparison is performed on the ordinal value of each character. To make this comparison more
understandable further two string objects are taken by taking different case in the last character only:
See the ordinal values of corresponding characters of each string objects:
From the output, we can conclude that, all the characters are same except the last one i.e., ‘o’ vs.
‘O’. The ordinal value of ‘o’ is 111, greater than uppercase O, whose ordinal value is 79. It means,
84
Python Fundamentals: A Functional Approach
string2 is less than string1. Like operator <, Python also allows other comparison operators for
string comparison based on ordinal values.
String objects can also compare with identity operator is and is not. As compared to the identity
operator, the comparison operator ‘==’ compares the values of string objects and checks for value
equality. Whereas is operator checks whether both the string objects (operands) refer to the same
memory reference or not. Consider the examples:
Let’s test the identity of two string objects with same value and modifying any of them and check
their memory reference with id for comparison:
Here, str1 and str3 both have same value as ‘Python 3.10’, but they are referring different memory
address. So is operator returns False, but comparison operator == returns True, because operator ==,
compares values of str1 and str3.
85
Python Fundamentals: A Functional Approach
Strings are zero (0) index-based, which means the first character can be accessed using the number 0
by passing it inside the square brackets ([]). For a string object 'python', indexing for each character are
shown below and these characters can be accessed using the index:
0 1 2 3 4 5
p y t h o n
>>> s = 'python'
>>> s[0], s[1], s[2], s[3], s[4], s[5]
('p', 'y', 't', 'h', 'o', 'n')
The index of the last character is one less than the length of the string. The len(s), will return the
number of characters and subtracting 1 from it will get the last valid index, as follows:
>>> s[len(s)-1]
'n'
String indices can also be specified with negative numbers, in which case indexing occurs from the end
of the string and the last character can accessed using index -1, and to refer the second-to-last character
by -2 and so on.
Attempting an index beyond the range as positive or negative results an error and the python
interpreter will raise an IndexError For example, the string 'Python', -6 is the lowest negative index and
5 will be the highest positive index.
The above indexing returns the substring ' is py' (from space at the index 4 to index 9, i.e. y).
The index of character t is 10, can get using str1[10]. It is obvious that, the indexing didn’t include
the character at index 10, because the slicing [4:10] returns 10-4=6 characters, not including the
end index. If we omit the start index python interpreter by default, consider zero (0) as a start index. It
means str1[0:end] is equivalent to the slicing str1[:end].
>>> str1[:10]
'This is py'
Like this, if we omit the end index it will consider the length of the string as last index. It means
str1[0:len(str1)] is equivalent to str1[0:]. For a given index n(0 ≤ n ≤ len(n)) the
expression str1[:n]+str1[n:] will return the string object as same as str1. The first operand
86
Python Fundamentals: A Functional Approach
(str1[:n]) of the expression returns a substring from index 0 to (n-1) and the second operand
(str1[n:]) will pick the characters from index n to len(info).
For this example, the value of n is considered as 5. So, the string str1 is sliced at 'This ' and
'is python language'. Later the plus ‘+’ operator performs the concatenation of these two
sliced strings. Omitting both the index will return the same string, not as a copy, it’s a reference to the
original string:
During slicing, if the start index is equal or greater than the end index then interpreter returns an empty
string. Negative indices can be used with slicing as well. -1 refers to the last character, -2 the second-to-
last, and so on, just like indexing.
>>> str1[3:3]
''
>>> str1[10:3]
''
>>> str1[-8:]
'language'
While slicing a string, a third value as step part of the index str[start : end : step],
representing a step count or a stride value can be used to get a substring. To add the step, count an extra
colon (:) is added after the stop index. This step value represents the number of character(s) that need
to be jumped between two consecutive characters of a string. See the examples given below:
>>> str1[::2]
'Ti spto agae'
Negative step value also allowed, in which case Python steps backward through the string. In this case,
the starting index should be greater than the ending index. See the examples given below:
Using negative stride value means the sliced string will be a reverse one, which means from the last
character of the string object. There is another use of using a negative stride value and specifically used
to reverse a given string. A string can be reversed by using any of these three techniques:
87
Python Fundamentals: A Functional Approach
▪ Considering the last index as the start index with -1 as the stride value str1[5∶∶-1]
▪ By providing only the stride value as -1 while skipping other indexes str1[:: -1]
▪ By providing -1 as the start index and stride value while skipping the end index str1[-1":: -
1]
>>> chr(65)
'A'
>>>ord('A')
65
>>>len('Hello')
5
Methods are similar to functions. A method is a specialized type of callable procedure that is tightly
associated with an object. Like a function, a method is called to perform a distinct task, but it is invoked
on a specific object and has knowledge of its target object during execution. The syntax for invoking a
method on an object is as follows:
object.method_name(argument(s))
In the above syntax, object represents any string object and method_name() will be any of the
method discussed in this section. Few of them may require single or more number of arguments or may
not and this list of methods further categorized based of their outcome.
88
Python Fundamentals: A Functional Approach
Methods related to case conversion such as upper to lower or title case or swapping the letter cases are
categorized into this group.
Method Description
str.capitalize() Converts the first character to uppercase
>>'python programming'.capitalize()
'Python programming'
>>'PYTHON'.lower()
'python'
>>'python'.upper()
'PYTHON'
>>'Python Programming'.swapcase()
'pYTHON pROGRAMMING'
>>'python programming'.title()
'Python Programming'
Python provides various string methods for searching a substring within a given string. Each method in
this group supports an optional start and end arguments. These are interpreted as string slicing and
the action of the method is restricted to the portion of the target string starting at position start and
proceeding up to but not including character position at end. If start is specified but end is not, these
methods apply to the portion of the target string from start to the end of the string.
Method Description
Returns an integer specifying the number of occurrences of the given
count(sub_string)
sub_string in a given string. Returns zero if sub_string is not found
>>'aab bba aab aab ba rta'.count('aa')
3
>>'aab bba aab aab ba rta'.count('bc')
89
Python Fundamentals: A Functional Approach
count(sub_string, start, end) - strat and end both are optional and specifying the start and end
index of sub_string within a given string. If end index is given then strat index become compulsory.
>>'aab bba aab aab ba rta'.count('aa',0,10) # with start and end index
2
>>'aab bba aab aab ba rta'.count('aa',5) # with start index
2
Returns True, if the given string starts with suffix, else return False.
startswith(suffix,
star, end) Other two parameters are optional and specifies the search area.
endswith(suffix,
Return True, if the given string ends with suffix, else return False.
star, end) Other two parameters are optional and specifies the search area
90
Python Fundamentals: A Functional Approach
index(sub_string, star, Search the sub_string within a given string and returns the
end) lowest index, same as find().
rindex(sub_string, star, Search the sub_string within a given string and returns the
end) highest index, same as rfind().
The only difference between index() and rindex() with find() and rfind() is, through an
exception as ValueErorr instead of -1, if the given sub_string is not found in the string, as follows:
Out of find() and index() method following points need to keep in mind when to prefer one over
another:
▪ If you are sure about the presence of a substring in the given string, you can use either of the
methods.
▪ If you are not sure about the presence of substring use find().
▪ In case, you are using the index() method to find the index of the substring, use exception
handling to avoid runtime crashes. The exception mechanism is discussed later.
Method Description
isalnum() Returns True if the string contains characters from A-Z or a-z or 0-9.
91
Python Fundamentals: A Functional Approach
isdecimal() - Return True if all characters in the string are decimal characters of base 10. Return False
for subscript and superscript numbers.
>>> '123'.isdecimal()
True
>>> two = '\u2082'
>>> "H"+two+"O"
'H₂O'
>>> two.isdecimal()
False
isdigit() - Returns True if the characters are from 0 – 9 only, irrespective of subscript or superscript
digits.
>>> '2.45'.isdigit()
False
>>> two.isdigit()
True
isnumeric() - Used to determine whether a string consists entirely of numeric characters. It covers basic
decimal digits, subscript and superscript digits, character with Unicode numeric value and currency numerators
>>> '123abc'.isnumeric()
False
>>> text = "\u0661\u0662\u0663" # Arabic-Indic digits
>>> text.isnumeric()
True
str.isidentifier() - Returns True if str, is a valid python identifier name, means satisfies naming
92
Python Fundamentals: A Functional Approach
>>> '_abc23'.isidentifier()
True
>>> '23'.isidentifier()
False
>>> 'else'.isidentifier()
True
>>> '@name'.isidentifier()
False
islower() Returns true if, the given string contains all the lowercase characters.
isupper() Returns true if, the given string contains all the uppercase characters.
isprintable() Returns true if the given string, contains all the printable characters.
A printable character is generally considered to be any character that can be displayed on the screen
or printed on a physical device. This includes: alphanumeric characters (a-z, A-Z, 0-9), punctuation
characters (!, @, #, $, %, etc.), space character. Nonprintable characters are those characters defined
in the Unicode character database as “Other” or “Separator”, excepting the ASCII space (0x20) which
is considered printable.
>>> '\t'.isprintable()
False
>>> ' '.isprintable()
True
>>> '\n'.isprintable()
False
>>> 'aA01@'.isprintable()
True
isspace() - Return True if there are only whitespace characters in the string and there is at least one
character, False otherwise.
istitle() - Return True if the string is a titlecased string and there is at least one character, for example
uppercase characters may only follow uncased characters and lowercase characters only cased ones. Return
False otherwise.
This set of methods used to manipulate the whitespace surrounding the text within the string. They are
particularly useful for formatting output or creating aligned columns of text.
center(width, fill) - Returns a string containing the given string as centred of the given width and
filled with default ASCII space character, else padded with fill character. Returns the given string, if the width
93
Python Fundamentals: A Functional Approach
expandtabs(tabsize) - Return a copy of the string where all tab characters are replaced by one or
more spaces, depending on the current column and the given tab size. It has an optional keyword argument
tabsize with default value 8.
If the character is a newline (\n) or return (\r), it is copied and the current column is reset to zero.
Any other character is copied unchanged and the current column is incremented by one regardless of
how the character is represented when printed.
>>>s = '01\t012\t0123\t01234'.expandtabs(4)
>>>len(s)
21
>>s
'01 012 0123 01234'
94
Python Fundamentals: A Functional Approach
In the above example, tabsize is set to 4 and till encounters a ‘\t’, considering the characters
‘01’ and next two spaces are considered, because tab position is 4. After that, out of 4 spaces 3 are
taken by ‘012’ so only one space is added and before the next ‘\t’ already 4 or more characters are
there and complete tabsize 4 is added.
join(iterable) - Return a string which is the concatenation of the strings inside an iterable object.
ljust(width, fillchar) - Return a string left justified and padded with the fillchar. Default is
ASCII space. Original string will return if width is less than or equal to the length of given string.
lstrip(char) - Return a copy of the string after removing the leading characters. If no char passed
then leading spaces are removed, else all the occurrences of given characters are removed.
String module provides many constants and can be used to verify string contains all the alphabets, digits,
punctuations, and whitespace etc. Table 5.2 listed all such constants with their purpose.
95
Python Fundamentals: A Functional Approach
hexdigits Returns a string containing all the hex digits 0-9 and a-f and A-F
octdigits Returns a string containing all the octal digits 0-7
punctuation String of ASCII characters which are considered punctuation characters
printable String of ASCII characters which are considered printable by combining
𝑑𝑖𝑔𝑖𝑡𝑠, 𝑎𝑠𝑐𝑖𝑖_𝑙𝑒𝑡𝑡𝑒𝑟𝑠, 𝑝𝑢𝑛𝑐𝑡𝑢𝑎𝑡𝑖𝑜𝑛 and 𝑤ℎ𝑖𝑡𝑒𝑠𝑝𝑎𝑐𝑒
whitespace A string containing all ASCII characters that are considered whitespace. This
includes the characters space, tab, linefeed, return, form feed, and vertical tab.
The 𝑖𝑚𝑝𝑜𝑟𝑡 𝑠𝑡𝑟𝑖𝑛𝑔 statement facilitate us to use any of these constants, as follows:
>>import string
>>string.ascii_letters
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>string.digits
'0123456789'
>>string.printable
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\
'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
>>string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
5.9 Formatting a String
Formatting a string is the process of infusing things in the string dynamically and presenting the string.
Python supports three ways to format a given string, as follows:
Formatting a string, requires to inject a format specifier based on the datatype listed in Table 5. 3.
While using % for string formatting is still supported in Python, it's considered an older approach.
Here's how to use it with the print function:
97
Python Fundamentals: A Functional Approach
String formatting with floating-point format-specifiers can be displayed in decimal and exponential
form.
>>> a = 0.000023
>>> b = 111111111111111111.56
>>> 'a = %f, b = %f' % (a, b)
'a = 0.000023, b = 111111111111111104.000000'
>>> 'a = %e, b = %e' % (a, b)
'a = 2.300000e-05, b = 1.111111e+17'
In the exponential form there will be a negative (-) or a positive (+) sign after e. A plus (+) sign indicates
a large value and minus (-) means represents a very small value. Further we can decide in advance the
width of the output, by specifying it with % sign. Field width is the width of the entire number and
precision is the width towards the right. One can alter these widths based on the requirements. The
default precision width is set to 6, for floating point numbers.
>>> a = 2.34
>>> print('a = %f' % a)
a = 2.340000
The default precision width for floating-point numbers using %f is indeed 6, as it shown in the output.
Based on our requirement the width of the precision can be controlled, as follows:
>>> print('a = %.3f' % a)
a = 2.340
In the print() statement a value 3 is set, after the decimal point (.) , that controls the width or length
of the precision. That’s why in the output a = 2.340 is printed, instead of a = 2.340000. The width
value should be placed in between % and f, and always be a positive integer. If the field width, is set to,
more than the default or required width, than the data will be right aligns itself to adjust to the specified
values.
>>> print('a = %f' % a)
a = 2.340000
>>> print('a = %5.3f' % a) #width 5 = as a’s length and precision,(3 < 6)
a = 2.340
>>> print('a = %5.9f' % a) #width 5 = as a’s length and precision,(9 > 6)
a = 2.340000000
>>> print('a = %6.3f' % a) #width 6 > a’s length and precision, (3 < 6)
a = 2.340
>>> print('a = %10.9f' % a) #width 10 > a’s length and precision,(9 > 6)
a = 2.340000000
Whenever we have used the precision value the output will be aligned either right or left. The first
output, aligned to left, because we have not used filed or precision width. But the second output, a value
5.3 is set, it means filed width = 5 and precision = 3. So the value will be printed up to 3rd decimal place
and the field width equal to the formatted numbers total length i.e. 5. That’s why aligned to left.
98
Python Fundamentals: A Functional Approach
In the 3rd print() statement, filed width = 5 and precision = 9. Because the width is same as length
then output will be left aligned but number of decimals placed is increased to 9 from 6. In the next line,
field width = 6 and precision = 3 and the output is right aligned by single space after equal sign. Because,
field width – precision length (3) – length of decimal part (1) – 1 (decimal
point) = 1. In the last print, field width (10) – precision (9) – length of decimal
part (1) – 1 (decimal point) = -1, output will be left aligned.
For proper alignment, a space can be left blank in the field width so that when a negative number is
used, proper alignment is maintained.
>>> b = -2.340
>>> print('% .3f' %b)
-2.340
>>> print('%+.3f' %a)
+2.340
Like a minus (-) sign get printed for a negative value, similar to this we can also print the plus (+) sign
for positive values, by inserting a plus (+) sign after the % sign.
Note: % formatting is not useful for larger string, because once you start using several parameters and
longer strings, your code will quickly become much less easily readable and things are starting to look a
little messy. To format long formatted string Python offers str.format() method.
A string can be formatted using str.format() method and formatted string contain “replacement
fields” surrounded by curly braces { }. Anything that is not contained in braces is considered literal text,
which is copied unchanged to the output. The syntax for a replacement field contains a filed name,
conversion character and a format specifier, as shown below:
replacement_field = "{"[field_name]["!"conversion][":"format_spec]"}"
In less formal terms, the replacement_field can start with a field_name, that specifies the object
whose value is to be formatted and inserted into the output of the replacement field. The field_name
is optionally followed by a conversion field, which is preceded by an exclamation point '!', and
a format_spec, which is preceded by a colon a ':'. These specify a non-default format for the
replacement value.
The field_name, itself begins with an arg_name, that is either a number or a keyword. If it’s a
number, it refers to a positional argument, and if it’s a keyword, it refers to a named keyword argument.
Consider examples given below:
99
Python Fundamentals: A Functional Approach
The string is formatted using arg_name only and inside the braces lan and ver is representing the
argument name. Instead of arg_name, position of the arguments can be used by specifying their
position inside the curly braces ({}) and by default an empty pair of braces {}, represents positional
only.
Further dictionary keys can be used as the position of the argument for formatting a string. See the
example given below:
The conversion field causes the type correction before formatting the given string. Formatting of any
value is done by the __format__() method of that value itself. However, in some cases, it is desirable
to force a type to be formatted as a string, by overriding its own definition of formatting. This ensures
the value is treated as a plain string and formatted according to the format specifiers you provide in the
string (like .2f for two decimal places). Details of all conversion flag that can be used to convert the
string to a specific type is listed in Table 5.4.
100
Python Fundamentals: A Functional Approach
The last format_spec, in replacement_field, contains a specification of how the value should
be presented, including such details as field width, alignment, padding, decimal precision and so on.
Each value type can define its own “formatting mini-language” or interpretation of the format_spec.
A format_spec field can also include nested replacement fields within it. These nested replacement
fields may contain a field name, conversion flag, and format specification, but deeper nesting is not
allowed. The replacement fields within format_spec are substituted before the format_spec string
is interpreted. This allows the formatting of a value to be dynamically specified. A general form of format
specifier is given below and in Table 5.5, listed every part of a standard format specifier with description.
With an align value any filled character can be preceded that can be any character and defaults to a space
if omitted. It is not possible to use a literal curly brace (”{” or “}”) as the fill character in a formatted
string literal or when using the str.format() method. Various alignment options are listed in Table
5.6:
Alignment Description
Options
‘<’ Forces the field to be left-aligned within the available space (this is the default for most
objects).
101
Python Fundamentals: A Functional Approach
‘>’ Forces the field to be right-aligned within the available space (this is the default for
numbers).
‘=’ Forces the padding to be placed after the sign (if any) but before the digits. This is used
for printing fields in the form ‘+000000120’. This alignment option is only valid for
numeric types. It becomes the default for numbers when ‘0’ immediately precedes the
field width.
‘^’ Forces the field to be centered within the available space.
Some examples of string formatting based on align value with predefined fill character and width
value (the length of the formatted string) is illustrated next, using keyword argument:
#Text left aligned with fill character and specified width with
>>'{text:{fill}{align}20}'.format(fill='#',text = 'AB',align='<')
'AB##################'
#Text centre aligned with fill character and specified width with
>>'{text:{fill}{align}20}'.format(fill='#',text = 'AB',align='^')
'#########AB#########'
Note: unless a minimum field width is defined, the field width will always be the same size as the data
to fill it, so that the alignment option has no meaning in this case.
Option Meaning
‘+’ indicates that a sign should be used for both positive as well as negative numbers.
‘−’ indicates that a sign should be used only for negative numbers (this is the default behavior).
Space indicates that a leading space should be used on positive numbers, and a minus sign on
negative numbers.
102
Python Fundamentals: A Functional Approach
>>>'{:-} {: } {:-}'.format(3.56,-2.52,-56)
'3.56 -2.52 -56'
The optional literal z forces negative zero floating-point values to positive zero after rounding to the
format precision. This formatting option is added in version 3.11. In many fields of mathematics
negative zero is surprising or unwanted minus (‘-’), especially in the context of displaying an (often
rounded) numerical result. Using this optional z, for floating-point presentation types (f, g, etc., as
defined by the format specification documentation), normalized to a negative zero to positive zero.
#with z
>>'{:z.2f}'.format(-0.0001)
'0.00'
#without z
>>'{:.2f}'.format(-0.0001)
'-0.00'
The '#' option causes the “alternate form” to be used for the conversion. The alternate form is defined
differently for different types. This option is only valid for integer, float and complex types. For integers,
when binary, octal, or hexadecimal format is used, this option adds the respective prefix '0b', '0o', '0x',
or '0X' to the output value. See the examples given below:
>>>'{:_b}'.format(1023456)
'1111_1001_1101_1110_0000'
>>>'{:_d}'.format(1023456)
'1_023_456'
>>>'{:_o}'.format(1023456)
'371_6740'
>>>'{:_X}'.format(1023456)
'F_9DE0'
Use of precision value with ‘f’ format specifier for fixed-point notation for displaying a specific
number of decimal places. While ‘g’ format specifier for general notation (choosing between fixed-
point or scientific notation for best readability), specifies number of significant digits (leading zeros don’t
count). In this case, it chooses fixed-point notation rounding to 3 significant digits.
>>>'{:.3f}'.format(2.456890000)
'2.457'
>>>'{:.3g}'.format(2.456890000)
'2.46'
For string precision field indicates the maximum field size - in other words, how many characters
will be used from the field content. The precision is not allowed for integer presentation types.
Type Meaning
‘s’ String format. This is the default type for strings and may be omitted.
B Binary format. Outputs the number in base 2.
C Character. Converts the integer to the corresponding unicode character before printing.
D Decimal Integer. Outputs the number in base 10.
‘o’ Octal format. Outputs the number in base 8.
‘x’ Hex format. Outputs the number in base 16, using lower-case letter.
‘X’ Hex format. Outputs the number in base 16, using upper-case letters.
N Number. This is the same as 'd', except that it uses the current locale setting (defined within
current system settings) to insert the appropriate number separator characters.
104
Python Fundamentals: A Functional Approach
In addition to the above presentation types, integers can be formatted with the floating-point
presentation types listed in (except 'n' and None) Table 5.9. When doing so, float() is used to
convert the integer to a floating-point number before formatting.
Table 5.9 Available presentation types for float and Decimal values
Type Meaning
‘e’ Floating point
‘E’ Scientific notation. Same as '𝑒' except it uses an upper case ‘𝐸’ as the separator character.
‘f’ Fixed-point notation
‘F’ Fixed-point notation. Same as 'f', but converts nan to NAN and inf to INF.
‘g’ General format
‘G’ General format. Same as 'g' except switches to 'E' if the number gets too large. The
representations of infinity and NaN are uppercased, too.
‘n’ Number. This is the same as 'g', except that it uses the current locale setting to insert the
appropriate number separator characters.
‘%’ Percentage. Multiplies the number by 100 and displays in fixed ('𝑓') format, followed by
a percent sign.
None For float this is the same as 'g', except that when fixed-point notation is used to format
the result, it always includes at least one digit past the decimal point.
In addition to the string’s format() method, a single object may also be formatted, using the syntax
format(object, formatspec), built-in function (which the method uses internally). The format
specifications listed in Table 5. 3 need to pass in place of formatspec inside format(), to get the
formatted string. Next some of the examples are illustrated to help you understand the working of
format() function.
>>format(13,'o')
'15'
>>format(2345.6,'.2f')
'2345.60'
>>format(10000000,',')
'10,000,000'
>>format(75,'#b')
'0b1001011'
>>format('wer','>20')
' wer'
>>format('wer','#>20')
'#################wer'
>>format(-0.,'z')
'0.0'
>>format(12,'%')
'1200.000000%'
105
Python Fundamentals: A Functional Approach
>>format(23.687,'e')
'2.368700e+01'
>>format(23.687,'g')
'23.687'
Strings in Python are usually enclosed within double quotes (“”) or single quotes (‘’). To create f-
strings, you only need to add character ‘f’ or ‘F’ before the opening quotes of the string. For example,
"This" is a string, whereas f"This" is an f-String.
When using f-strings to display strings, you only need to specify the names of the variables inside a
placeholder ({}). During the execution, all the names are replaced with their respective values. If you
have multiple variables in the string, you need to enclose each of the variable names inside the
placeholder. Suppose you have two variables namely language, and school, and a string can be
formed as follows:
>>language = 'Python'
>>school = 'Computer Science & Engineering'
>>message = f'We are learning {language} from {school}'
>>message
'We are learning Python from Computer Science & Engineering'
Notice how the variables language and school are replaced with their values. As f-Strings are
evaluated at runtime, you can evaluate valid Python expressions on the fly. In the next illustration, num1
and num2 are two variables. To calculate their product, you may insert the expression num1 * num2
inside a placeholder, and the expression will be evaluated, as follows:
>>num1, num2 = 3, 4
>>print(f'The product of {num1} and {num2} is {num1 * num2}')
The product of 3 and 4 is 12
>>name = "Eric"
>>profession = "comedian"
>>affiliation = "Monty Python"
>>message = (f"Hi {name}. "f"You are a {profession}. "f"You were in
{affiliation}.")
>>message
'Hi Eric. You are a comedian. You were in Monty Python.'
If we don’t put an f in front of each individual string, then placeholder {}, is considered as part of the
string. A string can also spread over multiple lines, by escaping a return with a backslash ‘\’:
The ‘f’, in f-strings may as well stand for “fast.”, f-strings are faster than both %-formatting, and
str.format(). Let’s see how to take care quotation mark within an f-string. While working with
quotations just make sure that, similar type of quotation mark on the outside of the f-string as you are
using in the expression, as follows:
>>f"{'Hello Python'}"
'Hello Python'
>>f'{"Hello Python"}'
'Hello Python'
>>f'''{"Hello Python"}'''
'Hello Python'
While formatting a dictionary within a string using f-string the double quotes needs to be surrounded
and keys must be retrieve using single quote. Using of single quote will leads to a SyntaxError, as follows:
There is a small difference between str.format() and f-strings. i.e., the difference is in how index
lookups are performed. In str.format(), index values that do not look like numbers are converted
to strings:
In .format() method, you need to use keyword arguments that match the placeholder names in the
string. Non-numeric keys (like variable names) are treated as literal strings within the curly braces. The
f-strings enable direct attribute access using string keys within curly braces {}. This means you can use
a variable name that corresponds to an object's attribute.
>>f'a={d["a"]}'
'a=10'
107
Python Fundamentals: A Functional Approach
This difference is required because otherwise you would not be able to use variables as index values:
>>a = 'b'
#using variable as key
>>f'a={d[a]}'
'a=20'
The .format() method allows you to insert values into strings using curly braces {}. However, it has
limitations on the types of expressions you can use within those braces. It's generally recommended to
use valid Python expressions within .format() to avoid unexpected behavior. If you need more
flexibility in formatting expressions, consider using f-strings.
Note: If you need more flexibility in formatting expressions, consider using f-strings n deep dive further
to PEP 49822, which covers other details about the f-string and str.format().
Key Takeaway’s
▪ Strings in Python are immutable, meaning once created, they cannot be changed.
▪ Use the + operator to concatenate strings.
▪ Access characters in a string using indexing with square brackets []. Indexing starts at 0.
▪ Retrieve parts of strings using slicing with the syntax [start : stop : step].
▪ During slicing, if start is omitted, it defaults to 0. If stop is omitted, it defaults to the length of
the string. If step is omitted, it defaults to 1.
▪ Negative values can be used for start, stop, and step. A negative index counts from the end of the
string.
▪ Slicing will not throw an error for indexes out of range; it will simply return an empty string or the
available characters.
▪ Use backslashes \ to insert special characters into strings, like \n for a newline or \\ for a backslash.
▪ Python has many built-in string methods for common tasks like .upper(), .lower(), .strip(),
.split(), and .replace().
▪ Use .format() or f-strings (formatted string literals) to embed expressions inside string literals.
▪ Prefix strings with r to treat backslashes as literal characters and not escape characters.
▪ Use triple quotes """ or ''' for strings that span multiple lines.
▪ Python strings are Unicode by default, which means they can include characters from most of the
world’s writing systems.
▪ To reverse a string use a negative step to reverse a string, like [::-1]
****************END OF CHAPTER***************
22
https://peps.python.org/pep-0498/#format-specifiers
108
6 BRANCHING AND LOOPING
In this chapter we will learn about types of statements supported by Python statement set. Table 6.1
summarizes the types of statements supported in Python and you have already seen top two i.e.
assignment and print in Chapter 3. Table 6.1 also highlights the respective Section of this book, where
we have explained the details about individual statement types. In your programs, these units of code
can perform actions, repeat tasks, to break out or continue with loop and so on.
This chapter deals with the entries listed in Table 6.1 and explains their working process based on
data type need to work with or depends on the situation. Statements lower in Table 6.1 i.e., def, class,
try/catch, and import that have to do with larger program units—functions, classes, modules, and
exceptions—lead to larger programming ideas, so we have not included here. But, def statements are
discussed separately in Chapter 8 and rest of them included in the Part 2 of this book. More focused
statements (like del, which deletes various components) are covered in Chapter 7.
Everything you have seen so far has consisted of sequential execution, in which statements are always
performed one after the next, in exactly the order specified. But the world is often more complicated
109
Python Fundamentals: A Functional Approach
than that. Frequently, a program needs to skip over some statements, execute a series of statements
repetitively, or choose between alternate sets of statements to execute. That is where control structures
come in. A control structure directs the order of execution of the statements in a program.
In the real world, we commonly evaluate information around us and then choose one course of action
or another based on what we observe. If the weather is nice, then I’ll mow the lawn. (It’s implied that if
the weather isn’t nice, then I won’t mow the lawn.)
In a Python program, we have used if statement to perform decision-making. It allows for
conditional execution of a statement or group of statements based on the value of an expression. This
section highlighted the working of if statement in following ways:
▪ First, you’ll get a quick overview of the if statement in its simplest form.
▪ Next, using the if statement as a model, you’ll see why control structures require some
mechanism for grouping statements together into compound statements or blocks. You’ll
learn how this is done in Python.
▪ Lastly, you’ll tie it all together and learn how to write complex decision-making code.
>>> if expr:
... statement(s)
110
Python Fundamentals: A Functional Approach
For this example, we have used Boolean values True and False with logical OR operator. The first if
expression evaluates to True and the print statement ‘Truthy Condition’ gets executed. But the logical
OR returns False, when both the operands evaluate to False and that is the reason, body of second if
statement is not executed means skipped or stepped over by the interpreter. Now let’s see a bit complex
expr and mix comparison and logical operator to form an expression within an if statement:
In the above example, two conditions 4 == 4 and 5 > 3 are combined using logical and operator.
Both the conditions are evaluating to True and the body of if, gets executed and displayed the message.
These is no such limit, how many such conditions we can combine using logical operators. Let’s solve
some problem to understand the working of if statement. All these are executed through Python script
file (.py). See Chapter 2, how to create and execute Python script files using IDLE.
Problem 6.1: Write a program to find the largest of two integers using if statement only and numbers
must be taken as input from user.
Enter number:23
Enter number:12
Numbers are : num1 = 23, num2 = 12
23 is largest
Two numbers 23 and 12 is taken as input and the expr in first if, num1 > num2 line 8, evaluates to
111
Python Fundamentals: A Functional Approach
True and displayed ‘23 is largest’. The expr, at line 8, written to test num1 (23) is larger than num2
(12), and the expr, evaluates to True. The second expr num2 > num1, at line 11, written for num2 is
larger than num1, becomes False for this pair of input. Till this discussion we have found that body of
if statement only executed if the expression of if evaluates to True. All the above examples have two if
statements, one written for Truthy condition and other one for Falsy condition. One can change the
pair of input, as shown below, and test the expr at line 11. So that num2 become largest.
Enter number:5
Enter number:6
Numbers are : num1 = 5, num2 = 6
6 is largest
In the above program multiple if statements are written for same purpose (to check the largest) and the
if statement, num1 > num2 at line 8, evaluates to True. From this point one can understand this, out of these
two if statements, one will be True and other will be False and need to skip. But Python interpreter, evaluates both,
because we have not written the code to do this. It can be achieved by introducing else (works for False
condition) clause with if statement. Before introduction to the else clause let us understand the
importance of indentation for if statement. Consider the coding sample illustrated next:
In the above code, if the condition num1 == num2, is True, then all the indented statements from line 6
to 8 get executed and then non-indented statement at line 9 will be executed. If the condition become
False, then program control will skip all the indented lines from 6 to 8 and directly executed line 9. See
the program output:
Enter number:4
Enter number:4
Numbers are : num1 = 4, num2 = 4
Start of If
Both numbers are same
End of If
For the same input, the condition at line 5, evaluates to True and all the indented lines inside if, gets
executed and then non-indented lines. Now execute the script again and give two different numbers as
input, as shown below:
Enter number:4
Enter number:5
112
Python Fundamentals: A Functional Approach
For different numbers as input and the condition at line 5, evaluates to False and program control
skipped all indented line inside if, and directly executes the non-intended lines after if. In both the
scenarios we have understood that, either way, execution proceeds with line 9 afterward. Did you notice
that there is no way to know the end of if block? Rather, put a print statement at the last intended line
inside if.
Note: In the Python documentation, a group of statements defined by indentation is often referred to
as a suite. This book uses the terms block instead of suite.
We have learnt, how to use an if statement to conditionally execute a single statement or a block of
statements. It’s time to find out what else can do. Sometimes, you want to evaluate a condition and
take one path of execution, if the condition with if statement it is true, and specify an alternative path
if it is not. This is accomplished with an else clause after an if. Let see the general syntax:
if expr:
statement(s)
else:
statement(s)
If the expr is true, the blocks of statements inside if, will be executed, and the statements inside else
will be skipped. If expr is false, then the first block is skipped, and the second one is executed. Either
way, execution then resumes after the second block, means else. Both blocks are defined by
indentation, as described above. Now let’s re-write the above program (Program 6.1) with if and else.
Problem 6.2: Write a program to find the largest of two integers using if-else statement and numbers
must be taken as input from user.
Enter number:8
Enter number:9
Numbers are : num1 = 8, num2 = 9
113
Python Fundamentals: A Functional Approach
Inside Else
9 is largest
See the solution carefully and the else clause at line 12, does not have any expr like if. It means else
not designed to test any condition. After running of the above program with 8 (for num1) and 9 (for
num2) as input, the if condition, num1 > num2, at line 8, evaluates to False, as we have entered largest
value for num2. So, it is obvious that the expression of if False and else block will be executed, thereafter
statements written after else will be execute. Earlier we have solved the same problem using if and now
we have re-written with else clause, which improves the code efficiency. Let us answer one question,
can we use an else clause without if? Answer is very straight a big NO; interpreter will raise a
SyntaxError.
Now let’s add another condition to the above program. Previously we have checked the largest of two
numbers using if and else clause. Now suppose we have given two of the same numbers as input and
let’s test the program is ready to tackle this. Now re-execute the above code and consider 5 as input for
both variables and analyze the code.
Enter number:5
Enter number:5
Numbers are : num1 = 5, num2 = 5
Inside Else
5 is largest
The output is ‘5 is largest’, but which one, num1 or num2. This happened because we did not write the
required logic to test the equality. Now question is that how to add the third condition in the program.
We can do this by replacing the else clause with an if and add third if statement to code, see the modified
code:
In the above code we have written three individual conditions based on their different outcome of the
problem. Means, if we take input num1 as the largest value, condition1 will become True and for num2
as largest and equality condition2 and 3 will be True. There is nothing wrong about the output of the
code, but the main drawback of this approach is all three conditions will be checked by the interpreter.
It is wasting of execution time and to improve we can use an else clause after any of the if statement, as
follows:
1 num1 = int(input('Enter number:'))
114
Python Fundamentals: A Functional Approach
The second if statement, at line 8 is replaced by an else clause, it means if the first if statement is True,
interpreter will skip the else clause. So, we have saved execution time, but, what if, the first if statement
is False, then else at line 9 and if statement at line 12 also evaluated (for same number). Have a look at
the output screen:
Enter number:5
Enter number:5
Numbers are : num1 = 5, num2 = 5
5 is largest
Both numbers are equal
We get two outputs instead of just ‘Both numbers are equal’. This happened because we have placed
the else clause after the first if. So, it makes the if statement at line 12 is an individual condition and
that’s why we get two outputs. To stop this one thing we can do, place the else clause after the second
if, as follows:
num1 = int(input('Enter number:'))
num2 = int(input('Enter number:'))
print(f'Numbers are : num1 = {num1}, num2 = {num2}')
For branching execution based on several alternatives we can use one or more elif (short for else if)
clauses. Python evaluates each expression in turn and executes the block corresponding to the first that
is True. If none of the expression is True, and an else clause is specified, then its body is executed.
Consider the required syntax for this:
>>> if expr:
... statement(s)
... elif expr:
... statement(s)
... elif expr:
... statement(s)
... ... ... ... ...
... ... ... ... ...
... else:
... statement(s)
We can place any number of elif (else if) clause, after if statement with an optional else clause at the
end. The actual benefit is we can interlink many conditions together, and on execution of any expr as
True, interpreter will skip the remaining elif and else (if provided) clause. At most, one of the code
blocks specified will be executed. If no else clause is included, then you might see no output, if all the
conditions are False, then none of the blocks will be executed.
Problem 6.3: Write a program to find the largest of two integers using if-elif-else statement and write
the required conditions to test the equality of two input numbers. Execute your code 3 times with
following pair of inputs (4, 5), (5, 4) and (5, 5) and the code shouldn’t produce more than one output.
116
Python Fundamentals: A Functional Approach
The above code is executed 3 times with different pair of inputs, as follows:
▪ num1 = 4, num2 = 5, output ‘num2 is largest’. For this pair of input, condition written at line
9 with elif clause evaluates to True and the first if expression is evaluating to False. Further, else
clause did not consider for execution.
▪ num1 = 5, b = 4, output is ‘num1 is largest’. For this pair of input, the if expression at line 6,
evaluates to True and interpreter skipped the elif and else clause.
▪ num1 = 5, b = 5, output is ‘Both numbers are equal’. Here all the conditions written with if and
elif clause is evaluates to False and block associated within else get executed.
Program 6.4: Write a Python program that takes a student's numerical grade as input and assigns a
corresponding letter grade based on the following criteria: a) 90 or above: 'A'; b) 80 to 89: 'B'; c) 70 to
79: 'C'; d) 60 to 69: 'D'; e) Below 60: 'F.
grade = float(input("Enter student's grade: "))
In the above code, total 5 conditions are listed and out of these, 1 condition is tested using if (for
grade 90 or more), 3 conditions are tested using elif statements (for grade >=60 and <90) and grade less
60 will be consider by else clause. For any given grade as input, program will produce the desired output
and rest of the conditions will skip by the interpreter.
In the above examples, all the if and elif expr tested only single condition and based on this any blocks
get executed. Sometimes it become necessary, to build a complex logic, where a decision is taken based
on combined conditions or based alternative of multiple conditions. The next Section is highlighted
about this fact.
To test multiple conditions that must be met together or to check for alternative conditions, we can
combine them in a single if or elif expression using logical (and, or) operators. This can improve code
readability and maintainability by avoiding the need for separate condition checks. The need of logical
operators can be summarized as follows:
▪ Combines Necessary Conditions: The and operator lets you check if all specified conditions
are met before executing a code block. This ensures that only intended actions are triggered
when all requirements are fulfilled.
▪ Checks for Alternatives: The or operator allows you to handle scenarios where at least one of
the combined conditions needs to be true. This simplifies the logic if multiple possibilities lead
to the same outcome.
117
Python Fundamentals: A Functional Approach
▪ Improved Readability: Logical operators help write concise if-elif expressions, making the
code more readable and easier to maintain compared to having separate checks for each
condition.
▪ Complex Decision-Making: By combining conditions, you can create more intricate decision
flows, making your programs more adaptable to various situations.
You can use logical (and, or) operators to combine multiple conditions or logical not to reverse the
decision, within an if or elif statement. The logical operators with if-elif are work as follows:
▪ and operator: The combined condition is only True if all individual conditions connected by
and are True. For e.g. if age >= 18 and has_id == True: (checks both age and ID for
access)
▪ or operator: The combined condition is True if at least one of the individual conditions
connected by or is True. For e.g. if is_weekend == True or is_holiday == True: (checks
for either weekend or holiday)
▪ not operator: Used to reverse the logical state of a single condition. not True is False, and not
False is True.
Program 6.5: Write a Python program that takes a student's numerical grade (floating-point number
between 0.0 and 100.0) as input. The program should then assign a corresponding letter grade based on
the following criteria:
▪ A: 90.0 to 100.0 (inclusive)
▪ B: 80.0 to 89.9 (inclusive)
▪ C: 70.0 to 79.9 (inclusive)
▪ D: 60.0 to 69.9 (inclusive)
▪ F: Below 60.0 (including invalid grades)
The if statement uses parentheses for proper evaluation order. The first condition (using or) checks if
the year is divisible by 4 but not by 100 (year % 4 == 0 and year % 100 != 0). The second condition
checks if the year is divisible by 400 (year % 400 == 0). If either condition is true, the year is a leap year.
Otherwise, it's not. These examples showcase how the logical OR operator (or) can be effectively
combined with if-elif statements to create more complex decision-making logic based on multiple
alternative conditions.
Sometimes we need to build complex logic, where multiple conditions need to be tested. Depend on
True outcome program execution will follow different path, and based on False, it goes in other
direction. To understand this, let’s consider a flow-chart of nested if-else statement.
In the flow chart you can see based on the outcome of condition, the program execution will follow
two different paths. If the condition1 become True, it will go for candition2, which will be nested inside
the condition1 if or statement else it will go for condition3. In Python programming we can implement
the same by nesting of an if, inside another if or inside an elif, or inside an else clause. This section
highlights the working of nested if-elif-else statements based on some examples.
A nested if-else statement is an if-else statement that is nested (meaning, inside) another if, elif or else
statement. With those statements we evaluate True or False conditions and make our program respond
appropriately. See the syntax of nested if-else structure:
>>> if expr:
119
Python Fundamentals: A Functional Approach
... if expr:
... statement(s)
... else:
... statement(s)
... elif expr:
... if expr:
... statement(s)
... ... ... ... ... ... ...
... ... ... ... ... ... ...
... else:
... if expr:
... statement(s)
... else:
... statement(s)
In the above syntax, we have nested if-else inside if and else block. Like this we can nest any number
if-else statements or only if inside an elif clause. Let’s rewrite the Leap Year Program using nested if-
elif-else statements.
Problem 6.7: Write a Python program that takes a year as input and determines if it's a leap year or not.
Developed your logic to satisfies the following rules:
▪ The given year must be divisible by 4 and then
o If the year is divisible by 100 such as 1700 is not a leap year
o If the year not divisible by 100 such as 2004 is a leap year
o If the year divisible by 100 and 400 such as 2000 is a leap year
▪ If the year is not divisible by 4 such as 2011 is not a leap year
In the above solution, code line number (4 – 10) are nested inside the if block written at line 4. Now,
these lines are part of the if block and these lines will be evaluated if the condition year % 4 == 0,
evaluates to True. This condition will divide the given year by 4, if the reminder of the division
operations returns zero, program control enters inside the if statement.
Next, interpreter have two situations, if the year for e.g. 1996, is not divisible by 100 and the condition
at line 4, evaluates to True, that year will be displayed as leap year. Further, the condition year % 400
== 0, nested inside the else statement.
Problem 6.8: Write a Python program to find the largest number out of three integers. Write your logic
to satisfy the equality of two and three numbers as well.
120
Python Fundamentals: A Functional Approach
The above code can be broken down into two segments as follows:
▪ Initial Equality Check (lines 6-7): Correctly states that the first segment tests for equality using
num1 == num2 == num3. Captures the essence of displaying "Numbers are equal" when all
three numbers are the same.
▪ Handling Number Differences (lines 8-26): Accurately identifies the else clause handling cases
where numbers are not equal. Clearly outlines the nested conditional structure for determining
the largest number.
It is permissible to write an entire if statement in one line. The syntax is given below:
>>> if expr: statement
121
Python Fundamentals: A Functional Approach
We can put more statements in same line, must be separated by semi-colon (;). Careful statements fall
in between colon (:) and last statement must be separated by semi-colon only.
>>> if expr: statement_1; statement_2; statement_3
If the given expression after if is True then all the statements followed by colon (:) will be executed
otherwise don’t execute any of them. All the statements after colon (:) considered as block of if. See the
example given below:
>>> if 5 > 4: print(1); print(2); print(3)
...
1
2
3
Note: While all of this works, and the interpreter allows it, it is generally discouraged on the grounds
that it leads to poor readability, particularly for complex if statements.
Since Python 2.5, it supports one additional decision-making entity called a conditional expression
(because the C language has a similar tool). In python, conditional expressions also known as conditional
operator or ternary operator. See the syntax given below:
>>> statement1 if expr else statement2
Python executes statement1, if the conditional expression (expr) evaluates to True, or executes
statement2, if expr, turns out to be False. We can also assign the result of conditional operator to a
variable. In ternary operator the else part is compulsory, and if we forget to put, interpreter will raise a
SyntaxError. Let’s see an example,
>>> age = 18
>>> s = 'minor' if age < 21 else 'adult'
>>> print(s)
minor
The above code tested a person is minor or adult based on his/her age. The variable age is holding a
value 18 and the condition age < 21, after if is evaluates to True. So, the statement1 i.e. ‘minor’ is
assigned to s and the statemen2 as part of else is ignored.
A common use of the conditional expression is to select variable assignment. For example, suppose
you want to find the smallest of two numbers. Of course, there is a built-in function, min(), which can
solve our purpose. But suppose you want to write your own code from scratch, as follows:
>>> a, b = 11, 12
>>> min = 0
>>> if a < b:
... min = a
... else:
... min = b
...
>>> print(min)
11
122
Python Fundamentals: A Functional Approach
With comparison to the above code a conditional expression will be shorter and more readable, as
follows:
>>> min = a if a < b else b
>>> print(min)
11
Note: The conditional expression behaves like an expression syntactically. It can be used as part of a
longer expression. The conditional expression has lower precedence than virtually all the other
operators, so parentheses are needed to group it by itself.
>>> a = b = 30
>>> c = 1+a if a < b else b+2
>>> c
32
Here (1 + a) and (b + 2) will be evaluated first then followed by the conditional expression (31 if
30 < 30 else 32) and 32 is assigned to the variable c. If we want to evaluate the conditional expression
first, then we need to put the conditional expression (a if a < b else b) inside parenthesis, as
follows:
c = 1 + (a if a < b else b) + 2
>>> c
33
The conditional expression a<b evaluates to False and the value of b i.e. 30 is replaced the conditional
expression. Then the expression become 1 + 30 + 2 evaluates to 33 and assigned to the variable c.
A match statement takes an expression and compares its value to successive patterns given as one or
more case blocks. A general syntax of match statement is given below:
match expression:
case match_exp1:
pass
case match_exp2:
pass
case _:
pass
At first, the expression will be evaluated and compared with every successive case expression written
as match_exp1 to match_exp2. On matching the body of respective case get executed. Note that, the
last case: is always written as _ (an underscored) acts as a wildcard and gets executed, if no case matches
and none of the branches is executed.
There are many situations where match expression become very useful. Like you want to perfume a
mathematical operation of any two number given by a user and the operation he wants to perform. See
the next programming example,
Problem 6.8: Write a program to take two numbers and an operator as input. Build your logic with
123
Python Fundamentals: A Functional Approach
match statement, to execute the desired mathematical operation, based on user choice.
a, b = eval(input('Enter two numbers:'))
operator = input('Enter an operator from +, -, *, /:')
match operator:
case '+':
print(a + b)
case '-':
print(abs(a-b))
case '*':
print(a * b)
case '/':
print(a / b)
case _:
print('Wrong operator selected')
Enter two numbers:4,5
Enter an operator from +, -, *, /:+
9
Enter two numbers:6,7
Enter an operator from +, -, *, /:3
Wrong operator selected
In the above code sample two scenario has been considered. At first, an addition operation between
4 and 5 is performed, based on the user selected operator. Once the first case expression matched,
statement(s) written as part of its body gets executes and prints 9 and all other case expressions are
ignored. Secondly, we have entered a wrong operator intentionally, which is not part of the any of the
case expression. Due to this the last case expression gets executed and prints ‘Wrong operator selected’.
We can also combine multiple literals in one case expression together using | (“or”) as stated below:
a, b = eval(input('Enter two numbers:'))
match a+b:
case 10|15:
print('First Case Executed')
case 20|25:
print(‘Second Case Executed’)
case _:
print(‘The Special case get Executed’)
Two points related to match statements are highlighted in the above example. At first, any mathematical
expression like ‘a+b’ can be used as part of match expression. Secondly, using “or” multiple case
expression can be combined together, which may be helpful when, we want to execute a group of
statement for different types case expression. Case expression look like unpacking assignments can be
used to bind variables in a match statement:
point = (3,0)
match point:
#for point = (0, 0)
case (0, 0):
print("Origin")
#for point(0, 3)
case (0, y):
print(f"Y={y}")
124
Python Fundamentals: A Functional Approach
#for point(3,0)
case (x, 0):
print(f"X={x}")
#for point = (3, 3)
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
The first pattern (0,0) has two literals and can be thought of as an extension of the literal pattern shown
above. But the next two patterns (0, y) and (x, 0) combine a literal and a variable, and the variable binds
a value from the subject (point). The fourth pattern captures two values, which makes it conceptually
similar to the unpacking assignment (x, y) = point.
An if clause can be added with a pattern, known as a “guard”. If the guard is false, match goes on to
try the next case block. Note that value capturing in case patterns happens before the guard is evaluated:
match point:
case point(x, y) if x == y:
print(f"Y=X at {x}")
case point(x, y):
print(f"Not on the diagonal")
Both match and if-else statements are used for conditional branching in Python, but they offer
different approaches:
▪ if-else Statements, uses Boolean expressions to evaluate conditions and execute a specific code
block based on whether the condition is True or False. It can also have chained elif statements
for handling multiple conditions.
▪ match Expressions (since Python 3.10+), performs pattern matching on an expression's value
and execute code blocks based on the matched pattern(s). It offers more flexibility and readability
for complex conditional logic.
▪ For error handling, if-else might require additional checks to ensure all conditions are covered.
match can optionally include a wildcard pattern (_) to handle unmatched cases for better error
handling.
▪ For simple condition, if-else is sufficient and familiar. But for complex pattern matching or
multiple conditions, match offers better readability and maintainability.
This section highlights about two Python’s looping constructs statements that repeat an action over and
over. At first, while loop is discussed to understand the basic mechanism of general loop constructs.
The second one is for loop statement, which is used to iterate over a collection of items and executes a
block of statement for each item.
While we are studying the loops, we will also cover some other statements such as break and
continue commonly used with loop. Along with this we will also cover some basic built-ins such as
125
Python Fundamentals: A Functional Approach
range, zip and map are commonly used with loop. In computer programming there are two types of
iteration, known as, definite and indefinite:
▪ The definite iterations mean, at the start of loop the number of iterations (number of times
designated block will execute) is defined.
▪ With indefinite iteration it is not known how many times the block of code will execute. Rather
the block of code is executed repeatedly as long as the condition is True.
Python while loop is the most basic construct of looping statement. As long as the condition at top
evaluates a true value the block (normally indented) will executes repeatedly. Once the top condition
become false the block of indented code will be skipped and interpreter proceed with other available
statements. Now let’s see the basic while loop statement.
>>>while expr:
statement(s)
The statement(s) generally refers block of statements, often referred as body of the loop, should
be indented just like the if statement. The control expression expr may contain any boolean expression
evaluates to True or False. If the given expr evaluates to True then the statements as part of block or
body of while will executes else loop terminates. Let’s consider the following while statement:
>>> n = 1
>>> while n <= 5:
... print(f'value of n is {n}')
... n = n + 1 #updating loop variable
...
value of n is 1
value of n is 2
value of n is 3
value of n is 4
value of n is 5
▪ The variable n is initialised to 1. The expr n<=5, at line 2, evaluates to True, and the body of
while from line 3 – 4 executes. Inside the body the value of n is printed and its value gets
incremented by 1, through the statement n = n + 1.
▪ The statement at line 4 is called an updation of the looping variable n. Once all the statements
from line 3 – 4 is executed, then program execution returns to the top of the loop at line 2, and
the condition n<=5 is evaluated again. This time the condition is tested over the updated value
of n, and it’s still True (because value of n is 2 which is less than or equal to 5). So, the body of
while executes again, and program control moves to top.
▪ This process will continue till the value of n become 6, which turns the expression evaluates to
False and loop terminates. Immediately program control moves to the statements followed the
loop body.
Some important points need to remember while working with while loop:
126
Python Fundamentals: A Functional Approach
▪ If we forget to update the loop control variable (n in our case), who’s value always tested before
entering to the body of the loop, then loop can be executed for infinite amount of time.
Consider the following while statement:
>>> n = 1
>>> while n <= 5:
... print(f'value of n is {n}')
Here the updation statement n = n + 1, is removed from the body of while. So, the expression
n<=5, always be True and due to this loop body will executes infinite amount of time.
▪ If we miss the initialization (n=1) statement then a NameError will raised, if we have used a
variable in the Boolean expression, which is not declared previously.
>>> while n <= 5:
... print(f'value of n is {n}')
... n = n + 1
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'n' is not defined
▪ It is also not necessary to update the loop control variable always incremented by 1. It can be
incremented or decremented by any value, as follows:
>>> n = 5
>>> while n > 0:
... print(n)
... n = n-1
Here the value of n isinitialised to 5, then at every iteration its value gets decremented by 1 (n
= n – 1). See, the next example which prints all the even numbers from 2 to 20, where loop
control variable gets updated by 2, after every iteration.
>>> n = 2
>>> while n <= 20:
... print(n)
... n = n+1
Now let’s see another example of while loop, with list, rather than a numeric comparison:
Problem 6.9: Write a program that asks the user for a positive integer (greater than 0). Keep prompting
the user until they enter a valid positive integer.
127
Python Fundamentals: A Functional Approach
status = True
while status:
number = int(input("Enter a positive integer: "))
if number > 0:
status = False
else:
print("Invalid input. Please enter a positive integer.")
print("You entered:", number)
Problem 6.10: Develop a program that plays a guessing game with the user. The program thinks of a
random number between 1 and 100 (inclusive). The user has to guess the number within a limited
number of attempts (e.g., 7 tries). After each guess, the program provides feedback like "Too high,"
"Too low," or "Correct!".
import random
if guess == secret_number:
print("Congratulations! You guessed the number!")
break # Exit the loop if the guess is correct
elif guess < secret_number:
print("Too low. Try again.")
else:
print("Too high. Try again.")
if guesses_left == 0:
print("Sorry, you ran out of guesses. The number was", secret_number)
A for loop is a generic iterator type in Python, used to step through the items in any ordered sequence.
We can use the for loop to iterate over a string, list, dictionary, set and other built-in iterables. Let us see
the general syntax of for loop:
In the given syntax iterable_object is a collection of objects of same type or different type such as a
list or set. The statement(s) inside the loop body is defined by the indentation just like if-else or while
statement. The loop body will execute once for each item var of the iterabable_object.
Here we can say that, every item of an iterable_obejct will be assigned to var at the start of the
iteration. Later in the body of loop we can use the value of var, if it is necessary. Let’s see an example:
▪ A list numbers is created at line 1 with 5 items. The for loop header at line 2 picks the first
item 1 from the list numbers and assigned it to number.
▪ Thereafter the body of loop from line 3 – 4 will be executed and the value of number get
incremented by 1.
▪ After that, value of number get printed and program control backs to the loop header at line
2, to iterate over the second item of the list. This process will continue till the length of the
numbers. Once all the items are iterated then program control will jump outside of the loop.
This program prints the output as 2, 3, 4, 5, 6 and Outside of for loop.
In Python iterable means an object can be used in iteration. If an object is iterable, it can be passed to
the built-in Python function iter(), which returns something called an iterator. Consider the statement
given below:
>>> iter(numbers)
<list_iterator object at 0x000002636D2665F0>
In the previous example we have used a list as part of iterable object, and python supports many other
iterables, such as set, tuple, dictionary, and strings etc. Some of them are provide sequential access to its
items and some of them are non-sequential.
Accessing a list, set, string or a tuple are similar, means at every iteration, we are getting the items
directly and for string we will get individual characters. But for dictionary, we are getting the keys only,
because in dictionary items are always stored in key:values pair. In dictionary, to access any value we
need the respective key. But for the other iterators such as string, list, tuple and set, we are directly
129
Python Fundamentals: A Functional Approach
getting the item. Use of any non-iterable object within for loop will raise a TypeError. A sample code
to accessing dictionary items are shown below:
>>> my_dict = {"name": "Alice", "age": 30, "city": "New York"}
>>> for key in my_dict:
... print(key, ":", my_dict[key])
...
name : Alice
age : 30
city : New York
This for loop that iterates through the keys in the my_dict dictionary. The key variable defines a variable
that can be used within the loop. This variable will store the key of each value of the dictionary item for
each iteration. Further, inside the for loop, we are accessing the value associated with the key,
my_dict[key] by passing the required key inside the square bracket ([]). This is not the only one way
to accessing a dictionary through for loop, the detail content is discussed in Chapter 7.
Whatever the examples we have seen so far in this chapter, it represents counter-style type for loop. In
comparison to while, for loop is quicker than while, so it should be our first-choice tool whenever we
need to iterate through a sequence or iterable objects.
Still there are some situations when we need to iterate over second or third items in a list or to update
the list. Sometimes we will see it also necessary to traverse more than one iterable object at the same
time. What if we need to access all the even or odd indexed item, where we need the index. To fulfil all
such requirement Python provides many built-ins for specialized iteration with for loop. These are as
follows:
▪ range(start, stop, step): Generates a sequence of numbers within a specified range.
Useful for creating the iteration variable in for loops.
▪ enumerate(iterable, start=0): Allows you to loop over both the items and their indices
in an iterable object, discussed in Chapter 7.
▪ zip(iterable1, iterable2, ...): Combines multiple iterables element-wise, allowing
you to loop through them together.
▪ sorted(iterable, key=None, reverse=False): Returns a sorted version of an iterable.
You can customize the sorting behavior with the key and reverse arguments, discussed in
Chapter 7.
▪ filter(function, iterable): used to create a new iterator containing elements from an
existing iterable, for which a certain condition is true. It applies a filter based on a user-
defined function.
▪ map(function, iterable): Applies a function to each element of an iterable and
returns an iterator of the results.
130
Python Fundamentals: A Functional Approach
former generated a list of those numbers all at once, while the latter produced numbers lazily, meaning
numbers were returned one at a time as they were needed.
Now let’s talk about how the range() function works and its totally depends how many values we
have passed to it. We can tell the range() function, about the starting and ending value and also
differences between one number and the next. There are three ways we can call the range() function:
▪ range(stop) takes one argument and stop informs about when to stop the iteration
▪ range(start, stop) takes two arguments and start informs about the starting value
▪ range(start, stop, step) takes three arguments, step control the differences between
two values generated by range().
The range(stop)
It will give range of values starts from zero (0), includes every number up to stop, but not including.
Values generated by range object can be used as index to iterate over any object, such as string, list, set
etc. Consider the statements given below:
In the output we have all the whole numbers starting from zero (0) up to but not including the stop (5).
With two arguments, we are controlling the numbers range from starts to stops. It means we don’t have
to start at 0 (default start value) all the time. We can use range() to generate a series of numbers from
predefined start to stop using range(start, stop). Let’s find out how to generate a range starting
at 1.
>>> for i in range(1,5):
... print(i, end = ' ')
...
1234
In the output, we have all the whole numbers from 1 (the number is provided as the start) up to but not
including 5 (the number you provided as the stop).
This range() function produces the numbers from 1 up to 6 (not including) and difference is 2,
controlled by the value of stop.
Note: The value of stop can be a positive or negative whole number, but not zero (0). If you try, Python
interpreter raised a ValueError.
Here we got a range of numbers that were each greater than the preceding number by 25, the step we
have provided. Now that you’ve seen how you can step forwards through a range, it’s time to see how
you can step backwards.
>>>for i in range(10,-5,-3):
print(i, end=’ ’)
10 7 4 1 -2
In the above example, the step value is -3. That means that you’ll be decrementing by 3, the new value
than its preceding value. Most importantly the start value must be greater than stop value. The most
Pythonic way to create a range that decrements is to use range(start, stop, step). But Python does
have a built-in reversed function, if you wrap range() inside reversed(), then you can print the
integers in reverse order.
>>>for i in reversed(range(5)):
print(I, end=’ ’)
43210
range() makes it possible to iterate over a decrementing sequence of numbers, whereas reversed() is
generally used to loop over a sequence in reverse order.
Problem 6.11: Write a program to find the sum n natural number start from 1 upto a given number
(inclusive) n, as input.
132
Python Fundamentals: A Functional Approach
if n <= 0:
print("Invalid input. Please enter a positive integer.")
else:
# Initialize sum to 0
sum = 0
Problem 6.12: Write a program to print the multiplication table of a given number by user.
num = int(input("Enter a number: "))
Enter a number: 5
5x1=5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
5 x 10 = 50
Problem 6.13: Write a program to take input 10 integers as list and build your own logic to find the
minimum and maximum number of this list.
1 numbers = []
133
Python Fundamentals: A Functional Approach
2
3 # Get 10 integers as input
4 for i in range(10):
5 number = int(input("Enter an integer: "))
6 numbers.append(number)
7
8 # Initialize min and max to the first element of the list
9 min_num = numbers[0]
10 max_num = numbers[0]
This program first creates an empty list number, uses a loop to get 10 integers as input from the user
and appends them to the list (line 4-6). Next, it initializes two variables, min_num and max_num, to the
first element of the list. This is because we can't be sure if the first element is the minimum or maximum
yet. The program then iterates through the list starting from the second element (since we already used
the first element to initialize min_num and max_num). Inside the loop (line 12-16), it compares each
element with min_num and max_num. If the element is smaller than min_num, it updates min_num with
the new value. Similarly, if the element is greater than max_num, it updates max_num. Finally, after iterating
through the entire list, the program prints the minimum and maximum numbers.
Now let’s look at a for loop that’s a bit more sophisticated than those we’ve seen so far. The next
example illustrates statement nesting a loop inside another loop. Given two lists of names with some
names are common in the lists. We need to create a third list containing the names present in both the
lists.
134
Python Fundamentals: A Functional Approach
The break and continue statements are used within looping statements (such as for and while loops) to
control the flow of the loop and alter its behavior. These statements serve distinct purposes in looping
environment.
Purpose: The break statement is used to terminate the execution of a loop prematurely when a certain
condition is met. It allows you to exit the loop before it naturally completes all its iterations.
Usage: When a break statement is encountered within a loop, the program immediately exits the loop,
and control flows to the statement immediately following the loop.
Purpose: The continue statement is used to skip the current iteration of a loop and move on to the next
iteration. It allows you to skip certain iterations of the loop based on a condition without exiting the
loop entirely.
Usage: When a continue statement is encountered within a loop, the program skips the remaining code
in the current iteration and proceeds to the next iteration.
135
Python Fundamentals: A Functional Approach
if i % 2 == 0:
continue # Skip even numbers
print(i)
This code will print only the odd numbers from 1 to 12, on meeting the condition, at line 2, i % 2
== 0, continue statement gets executed and program control will skip all the source line from part of
the for loop, will move to the next iteration of the loop.
Use Cases: continue is used when you want to skip certain iterations of a loop based on a condition,
such as filtering or processing specific elements in an iterable.
In summary, the break statement is used to prematurely exit a loop, while the continue statement is
used to skip the current iteration and move on to the next one. These statements provide control flow
mechanisms that allow you to tailor the behavior of loops based on specific conditions, making your
code more flexible and efficient.
The else keyword with loops serves a specific purpose: it provides a way to execute a block of code after
a loop has completed all its iterations without being interrupted by a break statement. The primary
purposes of using the else clause with loops are as follows:
▪ Checking for Completion: You can use the else clause to determine whether a loop completed
all its iterations without any premature termination using break. This can be useful for situations
where you want to act or make a decision based on whether a loop ran to completion or was
interrupted.
▪ Error Handling: It allows you to handle situations where a loop was expected to find
something or perform some action but didn't. You can use the else block to raise an exception,
log a message, or take other appropriate error-handling actions.
This behavior can be useful in certain situations to perform additional actions or checks after a loop has
finished running. The basic programming syntax to use else clause at the end of any looping statement
is given below:
for item in iterable:
# Loop body
if condition:
# Condition met, break out of the loop
break
else:
# This block is executed if the loop completes without a break
# Additional actions or checks can be performed here
The else clause must be written at the end of any looping construct. A loop can have any number of
if-else statements nested inside it, but the outer else clause (written at the end of loop) doesn’t have any
link with if-else written as part of the loop body as indented statement. The else clause at the end of
loop will be executed, if the condition for break statement didn’t meet, means for complete iteration of
the loop. The above claim about the working of else clause with loop will be better understand form the
next programming example.
Problem 6.14: Take a list of 10 numbers and take input a number from user to search it. Write your
logic and you are free to use any looping construct, but loop must be end with an else clause and print
136
Python Fundamentals: A Functional Approach
the message “Number is not found”. Else the loop body must terminate, if the given input is present in
the list and displayed “Number is present at index”.
numbers = [11,23,45,8,2,9,8,67,3,5]
else: # Execute only if loop completes without a break (number not found)
print("Number is not found")
In this example a number is searched and for that it is compared with all the numbers present in a list
object. If the search_number is present, then else block will not executed. In this example, the else
block is executed only if the loop completes all iterations without finding the search_number. It
provides a clear way to communicate whether the search was successful or not.
Problem 6.15: Build a required logic to test a password is satisfying the following criteria or not.
Password must be taken as input from user and maximum three attempts are allowed to form a correct
password. Write the required else clause with while loop and print “Password creation successful”, if
user form a password within 3 attempts, else display “Maximum attempted reached”. The condition for
password is given below:
▪ The length of the password minimum 8 and maximum 15.
▪ Password must contain one lowercase, uppercase, special case, and number from 0-9.
import string
max_attempts = 3
attempts = 0
else:
print("Maximum attempts reached. Password creation failed.")
Using the else clause with loops can make your code more expressive and help you handle situations
where you want to check if a certain condition was met during the loop's execution. In summary, the
else clause in loops in Python is a way to handle situations where you want to execute code after a loop
has completed all its iterations without interruption, and it helps improve the readability and
maintainability of your code in such scenarios.
138
Python Fundamentals: A Functional Approach
▪ Stubbing out loop bodies: You can use pass to define an empty loop body when you're in the
process of designing your code and haven't implemented the logic yet. This allows you to create
the loop structure without writing the actual code at that moment.
>>> for item in iterable:
... #Add your logic
... pass
▪ Placeholder for Future Code: When you're working on a codebase collaboratively or planning
to add specific functionality to a loop later, you can use pass as a placeholder to indicate that
this is an intentional empty section that will be filled in later.
for item in iterable:
if condition:
# Code to handle the condition
else:
pass # Placeholder for future code
▪ Maintaining Code Structure: In some cases, you may want to maintain the structure of your
code even if you don't currently need to perform any action within a loop. Using pass in such
cases keeps the loop intact and doesn't break the code's structure.
▪ Simplifying Conditional Logic: pass can be used in conditional blocks inside loops to
maintain consistent indentation and structure, especially when the true or false branches of the
condition don't require any additional code.
Key Takeaway’s
▪ Python uses indentation to define blocks of code. Ensure that the code inside the if and else
blocks is properly indented.
▪ After the if or else statement, a colon (:) is required to indicate the start of the block.
▪ The condition after if must evaluate to a Boolean value (True or False). You can use comparison
operators (==, !=, <, >, <=, >=) to form these expressions.
▪ Use elif (short for else if) to check multiple conditions in sequence.
▪ In Python, non-zero numbers, non-empty strings, and non-empty lists are considered True.
Zero, empty strings/lists, and None are considered False.
▪ For simple conditions, you can use a one-line if-else statement, known as a ternary operator: x
= 'True' if condition else 'False'.
▪ for loops are used for iterating over a sequence (like a list, tuple, dictionary, set, or string).
▪ Use the range() function to generate a sequence of numbers which can be iterated over
▪ Use break to exit the loop prematurely if a condition is met.
▪ Use continue to skip the current iteration and continue with the next one.
▪ An else block after a for loop will execute after the loop completes normally (i.e., not exited
with a break).
▪ while loops continue to execute as long as a condition remains true.
Practicing Question
1. Write a program that takes the current temperature as input and gives advice on whether to
wear a jacket, a t-shirt, or a sweater based on the temperature range. You can take following
rages of values, or you can go with your own conditional values:
▪ If temperature less than 10 print “It's cold! Wear a jacket.
▪ If temperature is in between 10 and 20 “It's cool. You should wear a sweater”
139
Python Fundamentals: A Functional Approach
****************END OF CHAPTER****************
140
7 EXPLORING PYTHON COLLECTIONS
7.1 List
In short, a list is a collection of arbitrary objects, somewhat like an array in many other programming
languages but more flexible. Lists are defined in Python by enclosing a comma-separated sequence of
objects in square brackets ([]), as shown below:
>>>nums = [3,30,6,7,23]
>>>nums
[3, 30, 6, 7, 23]
It is also possible to create an empty list by assigning a pair of empty square brackets ( []) or using
list() constructor method, as follows:
>>>nums = []
>>>len(nums)
0
>>>nums = list()
>>>len(nums)
0
len() returns the number of items present in a list and returns 0, on passing an empty list. Out of these
two approach to create an empty list, the second approach list() is slower because it requires looking
up the name of the function, calling it, and then creating the list object in memory. In contrast, [] is
like a "shortcut" that doesn't require so many intermediate steps to create the list in memory.
141
Python Fundamentals: A Functional Approach
▪ Store a sequence of values: Lists are a great choice when you need to store a sequence of
related values. For example: list of names, mobile numbers, etc.
▪ To update your records: lists are mutable by nature means items of the list can be modified
any number of times.
▪ To access items randomly: Lists allow quick and easy access to elements based on their index.
List index started form 0, means the location of first item and supports negative indexing also.
There are sometimes we need to ignore list object to store some specific type of data:
▪ Don’t use to store immutable data, as list is mutable in nature. Prefer tuple for immutable data
and tuples are more memory efficient.
▪ If list is used to represent database records, then data can be easily modifiable. So, consider
using a tuple or a data class.
▪ As list allows duplicate data, so it is not preferable to store unique and unordered values.
▪ Run many membership tests where item doesn’t matter: In this case, consider using a set. Sets
are optimized for this type of operation.
▪ To work with advanced array and matrix operations consider using NumPy’s specialized data
structures.
▪ For manipulate your data as a stack or queue consider using deque or Queue from collection
module.
7.1.2 Accessing and Updating list items
You can access individual items from a list using the item’s associated index. What’s an item’s index?
Each item in a list has an index that specifies its position in the list. Indices are integer numbers that
start at 0 and go up to the number of items in the list minus 1. The following syntax is used to access
the list item using index:
>>>l1 = [4, 4, 2, 1, 4]
>>>l1[0]
4
Instead of accessing just one item, we can index multiple items by passing range of indexes. Some of the
examples related to range of indexing is listed in Table 7.1. Consider a list my_list = [1, 2, 3, 4,
5].
142
Python Fundamentals: A Functional Approach
1 l1 = [4, 4, 2, 1, 4]
2 for i in range(0,len(l1)):
3 print(l1[i],end=' ')
range(0, len(l1)) generates the indexes from 0 to (length -1) of the list. L1[i] accessing the items
of l1 using index the value of ‘i’. List items are also accessible using negative index. Negative indexing
means start from the end, -1 refers to the last item, -2 refers to the second last item etc.
1 l1 = [4, 4, 2, 1, 4]
2 for i in range(-1,-len(l1)-1,-1):
3 print(l1[i],end=' ')
41244
range() in line 2 generates the negative index starting from -1 to -len(l1) and len[i] accessing
items from back of the list. Without index also items are accessible using a loop, as shown below:
1 my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
2 for x in my_list:
3 print(x, end=' ')
123456789
But the above style of accessing, doesn’t allow us to update the list item. You can loop through the list
143
Python Fundamentals: A Functional Approach
items by using a while loop as well. Use of len() function to determine the length of the list, then start
at 0 and loop your way through the list items by referring to their indexes. Remember to increase the
index by 1 after each iteration.
1 my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
2 i = 0
3 while i < len(my_list):
4 print(my_list[i], end=' ')
5 i = i + 1
In many situations, you’ll need to know the index of the current item to be able to perform some
computations. It’s especially true when you’re dealing with complex algorithms that operate on indices.
In those cases, Python has you covered as well. It offers you the built-in enumerate() function, which
you can use as in the following example:
Python provides many other tools that you can use when you’re iterating through a list of values. For
example, you can use reversed() to iterate over the list in reverse order:
The reversed() function to traverse your list of fruits in reverse order, which might be a common
requirement in your code. Another common need is to traverse the list in sorted order. For this, you can
use sorted() as in the code below:
1 numbers = [2, 9, 5, 1, 6]
2 for number in sorted(numbers):
3 print(number, end=' ')
1 2 5 6 9
The zip() function, which allows you to iterate over multiple lists in parallel. The zip() function
returns an iterator of tuples. The elements of each tuple come from the input iterables. In this example,
the tuples combine items from three lists integers, letters, and floats.
144
Python Fundamentals: A Functional Approach
1 integers = [1, 2, 3]
2 letters = ["a", "b", "c"]
3 floats = [4.0, 5.0, 6.0]
4
5 for i, l, f in zip(integers, letters, floats):
6 print(i, l, f)
1 a 4.0
2 b 5.0
3 c 6.0
Whenever you use the concatenation operator, you get a new list object as a result. Consider the
following example and keep an eye on the identity of digits list, that after every concatenation its
identity get changed:
>>>digits = [0, 1, 2, 3, 4, 5]
>>>id(digits)
3028851826880
>>>digits = digits + [6, 7, 8, 9]
>>>id(digits)
3028851824384
You can only concatenate a list with another list. If you try to concatenate a list with something else,
then Python will raise a TypeError exception:
145
Python Fundamentals: A Functional Approach
>>>[0, 1, 2, 3, 4, 5] + (6, 7, 8, 9)
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
[0, 1, 2, 3, 4, 5] + (6, 7, 8, 9)
TypeError: can only concatenate list (not "tuple") to list
The concatenation operator has an augmented variation, which uses the += operator. The augmented
concatenation operator works on an existing list. It takes a second list and adds its items, one by one, to
the end of the initial list. The operation is a shortcut to something like digits = digits + [6, 7, 8,
9]. However, it works a bit differently. Unlike the regular concatenation operator, the augmented
variation mutates the target list in place rather than creating a new list:
>>>digits = [0, 1, 2, 3, 4, 5]
>>>id(digits)
3028851821952
>>>digits += [6, 7, 8, 9]
>>>id(digits)
3028851821952
In this example, the id() function returns the same value in both calls, meaning that you have a single
list object instead of two.
Repetition consists of cloning the content of a given list a specific number of times. You can achieve
this with the repetition operator (*), as follows:
In the above example, you repeat the content of a list three times and get a new list as a result. The right-
hand operand is an integer representing the number of times that you want to repeat the list’s content.
You can also face the need of comparing two lists and it can be done using the standard comparison
operators. All these operators work by making item-by-item comparisons within the two involved lists,
as follows:
>>>[2, 3] == [2, 3]
True
>>>[5, 6] != [5, 6]
146
Python Fundamentals: A Functional Approach
False
>>>[5, 6, 7] < [7, 5, 6]
True
>>>[5, 6, 7] > [7, 5, 6]
False
>>>[4, 3, 2] <= [4, 3, 2]
True
>>>[4, 3, 2] >= [4, 3, 2]
True
Lists with different lengths can also compared and in this case all the items of first list (left operand) will
be compared with righthand side operand, based on the given operator.
True
>>>[5, 6, 7] == [5]
False
In the first expression, you get True as a result because 5 is less than 8. That fact is sufficient for Python
to solve the evaluation. In the second example, you get False. This result makes sense because the lists
don’t have the same length, so they can’t be equal.
Python has a set of built-in methods that you can use on lists. To add an item to a list the append()
and insert() method can be called on a list object.
>>>nums = [1,2,3]
>>>nums.append(6)
>>>nums
[1, 2, 3, 6]
>>>nums.insert(1,7)
>>>nums
[1, 7, 2, 3, 6]
nums.append(6), appends an object at last and nums.insert(1,7) requires two parameters. The first
parameter is ‘1’, that specifies the index or location where to be inserted the given object ‘7’ as second
parameter. To remove an item or all from a list object, we can use clear(), pop(), and remove()
methods, as follows:
147
Python Fundamentals: A Functional Approach
>>>nums.remove(2)
>>>nums
[1, 7, 3, 6]
To remove any specific item from a list, we can pass any value to nums.remove(2), if the item is present,
else Python will raise a ValueError. To handle this issue one can first check the membership of an item,
if present then it can be removed.
#7 remove
>>>if 7 in nums: nums.remove(7)
>>>nums
[1, 3, 6]
To remove any item from a specific location pop() method is used, by passing the index to it. If the
given index is equal or more than the length of a list, it will raise an IndexError. To handle this, you can
first check the length of the list and then pass an index value lesser than the length.
>>>index = 9
>>>nums.pop(index) if index < len(nums) else print('Invalid index')
Invalid index
>>>index = 2
>>>nums.pop(index) if index < len(nums) else print('Invalid index')
6
>>>nums
[1, 3]
Using .clear() all the items of a list can be removed and the list object will be empty.
>>>nums.clear()
>>>nums
[]
List index() method searches for a given element from the start of the list and returns the index of the
first occurrence. Like remove(), index() can also raise a ValueError, if the given value is not present
in the list object.
148
Python Fundamentals: A Functional Approach
>>>nums = [2,4,5,6,7]
>>>nums.index(3)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
nums.index(3)
ValueError: 3 is not in list
>>>nums.index(4)
1
ValueError can be taken care by using an extra check that we did earlier for remove(). Instead of
searching all over the complete list, we can also pass a range of indexes using start and end value to the
index() method, as follows:
>>>nums = [1, 2, 3, 4, 1, 1, 1, 4, 5]
>>>nums.index(4,3,6)
3
The code uses Python's built-in .index() method to find the index of the number 4 within a specific
range of a list. the index method is searching for the first occurrence of the number 4 between indices
3 and 5 (because 6 is exclusive). Since 4 is found at index 3, which is within the specified range, the
method returns 3 as the output. If the value 4 wasn't found within the specified range (3 to 5), a
ValueError exception would be raised.
The .count() method in Python is a built-in function used to count the number of times a specific
element appears in a list.
>>>points = [1, 4, 2, 9, 7, 8, 9, 3, 1]
>>>points.count(9)
2
The above code uses the .count() method to find the frequency of 9 within points. The count method
then searches the entire points list and returns the number of times it finds the value 9.
The .copy() method returns a shallow copy of the original list. It creates a new list object that
contains the same elements as the original list, but any modifications made to the new list will not affect
the original list.
>>>l1 = [4, 4, 2, 9, 7, 8]
>>>l2 = l1.copy() #creates a shallow copy
>>>l2[0] = 1
>>>l2
[1, 4, 2, 9, 7, 8]
>>>l1
[4, 4, 2, 9, 7, 8]
After the copy operation, first item at index 0, in list l2 is updated, but original list l1 didn’t get effected.
Instead of .copy(), if the assignment operator is used, then it creates a deep copy of the original list.
Means changes made inside the new list, will modifying the actual list. See the illustration below:
>>>l1 = [4, 4, 2, 9, 7, 8]
>>>l2 = l1 #creates a deep copy
>>>l2[0] = 1
>>>l1
[1, 4, 2, 9, 7, 8]
149
Python Fundamentals: A Functional Approach
>>>l2
[1, 4, 2, 9, 7, 8]
The .extend() method adds the specified list elements (or any iterable) at the end of the current list.
In the illustration, items of l2 added at the end of l1, on which .extend() is called.
>>>l1 = [4, 4, 2]
>>>l2 = [1, 4]
>>>l1.extend(l2)
>>>l1
[4, 4, 2, 1, 4]
The reverse() method is used to reverse the order of elements in a list. It is an in-place reversal method,
meaning it modifies the original list and does not create a new one, making it memory-efficient.
>>>l1.reverse()
>>>l1
[4, 1, 2, 4, 4]
The sort method is used to sort elements in a list. By default, it sorts the list in ascending order. You
can optionally customize the sorting behavior using two arguments:
▪ reverse: This is a boolean value (True or False). If set to True, the list will be sorted in
descending order.
▪ key: This is a function that takes one element from the list as input and returns a value to be
used for comparison during sorting. This allows you to define custom sorting criteria.
>>>l1 = [4, 4, 2, 1, 4]
>>>l1.sort() #ascending order
>>>l1
[1, 2, 4, 4, 4]
>>>l1.sort(reverse=True) #descending order
>>>l1
[4, 4, 4, 2, 1]
The key parameter allows you to specify a function that computes a value for each element in the list,
and the list is then sorted based on the computed values rather than the elements themselves. See the
illustration:
In the above example list is sorted based on the length of each string. To do that the len() function is
used as the key parameter, which computes the length of each string. The sort() function then uses
these computed lengths to sort the list. if you want a case-insensitive sort function, use str.lower() as
a key function
>>>fruits.sort(key = str.lower)
>>>print(fruits)
['banana', 'cherry', 'Kiwi', 'Orange']
Program 7.1: Design a user-defined function to sort a list of fruit names based on the last letter of each
fruit name.
sort_by_last_letter() function takes a single argument word (which is a string in this case) and
returns the last character of the word. This function essentially defines the sorting criteria.
words.sort(key=sort_by_last_letter) line sorts the words list using the sort method. The key
argument specifies the sort_by_last_letter function. This means the sorting will happen based on the
last letter of each word (as determined by the sort_by_last_letter function). As you can see, the words
are sorted alphabetically based on their last characters (t, a, y, e, and e).
You can also take advantage of some Python functional programming tools, such as map() and
filter(), to traverse a list of values. These functions have an internal loop that iterates over the items
of an input iterable and returns a given result.
The map() function takes a transformation function and an iterable as arguments. Then it returns
an iterator that yields items that result from applying the function to every item in the iterable.
In the above illustration, map() applies int() to every item in numbers in a loop. Because map()
returns an iterator, to the list() constructor to consume the iterator and show the result as a list.
If you need to filter out some values from an existing list, then you can use the built-in filter()
function. This function takes two arguments: a predicate function and an iterable of data. Then it returns
an iterator that yields items that meet a given condition, which the predicate function tests for. Here’s
how filter() works in practice:
151
Python Fundamentals: A Functional Approach
In the above illustration map() is used when you want to transform every item in an iterable using a
function and collect the results. filter() is used when you want to select and retain specific elements
from an iterable based on a condition. The built-in min() and max() takes a list as argument and returns
the smallest and largest of any list.
>>>l1 = [3,4,5,12,7]
>>>min(l1)
3
>>>max(l1)
12
Sometimes in programming it is necessary to check the truthiness of a list and for that Python offers
two built-in functions as any() and all(). Refer the programming example 7.2 to understand the
working of these functions with list object.
Program 7.2: Suppose you want to schedule interviews for all the candidates, who meet any or all of
the following criteria, (a) Knows Python; (b) Have experience five or more years; and (c) Have a degree
applicants = [
{
"name": "Devon Smith","programming_languages": ["c++", "ada"],
"years_of_experience": 1,"has_degree": False,
"email_address": "[email protected]"},
{
"name": "Susan Jones","programming_languages": ["python", "javascript"],
"years_of_experience": 5,"has_degree": True,
"email_address": "[email protected]"},
{
"name": "Sam Hughes","programming_languages": ["java"],
"years_of_experience": 4,"has_degree": True,
"email_address": "[email protected]"},
]
for applicant in applicants:
meet_criteria = []
meet_criteria.append("python" in applicant["programming_languages"])
meet_criteria.append(applicant["years_of_experience"] >= 5)
meet_criteria.append(applicant["has_degree"])
if any(meet_criteria):
print(f'{applicant["name"]}')
Susan Jones
Sam Hughes
At first a list of dictionaries namely applicants are created, where each candidate information is
152
Python Fundamentals: A Functional Approach
if all(meet_criteria):
print(f'{applicant["name"]}')
Susan Jones
From the output we can check only ‘Susan Jones’ has matched all the criteria and any() returns True,
if all the items of a given list are True.
List comprehension is a syntactic construct used to create a new list from an existing list. It follows the
form of the mathematical set-builder notation (set comprehension) as distinct from the use of map and
filter functions. Consider the following example in set-builder notation:
153
Python Fundamentals: A Functional Approach
𝑆 = {2 ∙ 𝑥|𝑥 ∈ ℕ, 𝑥 2 > 3}
or often
𝑆 = {2 ∙ 𝑥: 𝑥 ∈ ℕ, 𝑥 2 > 3}
This can be read as, S is the set of all numbers, 2 times of x, where x is an element or member of set
natural number ℕ, and x square is greater than 3. The smallest natural number is 1 and the condition
12 > 3, fail to satisfy the condition so it is not included in the new list, else all other numbers are added.
The given set-builder notation can be annotated as follows,
𝑆={ 2
⏟ ∙𝑥 | ⏟
𝑥 ∈ ℕ
⏟ 𝑥 2 > 3}
,⏟
𝑜𝑢𝑡𝑝𝑢𝑡 𝑒𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛 𝑣𝑎𝑟𝑖𝑎𝑏𝑙𝑒 𝑖𝑛𝑝𝑢𝑡 𝑠𝑒𝑡 𝑝𝑟𝑒𝑑𝑖𝑐𝑡
where,
▪ 𝑥 is the variable representing numbers of an input set or a list.
▪ ℕ represents the input set, in this example we have taken the set of natural number.
▪ 𝑥 2 > 3, is a predictor used as a filter out some items from existing list or set
▪ 2 ∙ 𝑥 is an output expression producing member for new list or set
▪ Symbol {} indicates that the result is a set
▪ The vertical bar | is read as such that
▪ Comma (,) separates the predict and read as AND
A list comprehension has the same syntactic components to represent generation of a list in order from
an input list or iterator:
In line 2 of the above illustration, if letter “a” is in name having the value as a fruit names from fruits
iterator, is the predictor used and worked as filter out some fruits name from the given iterable object
fruits and fruit names are used as part of the expression. If we want to generate the same list without
154
Python Fundamentals: A Functional Approach
list comprehension it needs more than one line of source code and a conditional statement need to be
put inside for statement, as follows:
1 fruits = ["apple", "banana", "cherry", "kiwi", "mango"]
2 newlist = []
3
4 for fruit_name in fruits:
5 if "a" in fruit_name:
6 newlist.append(x)
7
8 print(newlist)
It is not necessary to use any predicate rules of expression as part of list comprehension. The next
example, generates a new list without the using of any predicate rule. But as part of the expression each
item of the given list is squared with the expression number**2.
>>>numbers = [3,4,5,6,8]
>>>nums = [number**2 for number in numbers]
>>>print(nums)
[9, 16, 25, 36, 64]
In list comprehension a condition is act as a filter that only accepts the items, if it evaluates to True. See
the next illustration:
>>>numbers = [3,4,5,6,8]
>>>nums = [number**2 for number in numbers if number % 2 == 0]
>>>print(nums)
[16, 36, 64]
The condition if number % 2 == 0, will be True for all the numbers are even, making a new list
containing the square of all the even numbers. As part of the iterable object anything can be used for a
sequence type. It is also possible to generate a sequence through range() function:
You can also use a string as an iterable and generating a list of letters of a given string, using
comprehension:
The expression char is the current item in the iteration, but it is also the outcome, which you can
manipulate before it ends up like a list item in the new list. The next example converting all the fruits
name to upper case:
>>>fruits = ["apple", "banana", "cherry", "kiwi", "mango"]
>>>[x.upper() for x in fruits]
['APPLE', 'BANANA', 'CHERRY', 'KIWI', 'MANGO']
155
Python Fundamentals: A Functional Approach
You can also generate a new list containing all the same item of equal length of existing one, as shown
below:
The expression can also contain a condition, not like a filter, but as a way to manipulate the outcome.
The given condition, if x != "banana" checks if the current element (x) is not equal to "banana". If
it's True, then the current element (x) itself is included in the new list. If it's False (x == "banana"),
then "orange" is included in the new list instead.
>>>A = [[1,2],[3,4]]
>>>A
[[1, 2], [3, 4]]
The 2D list is enclosed within double square brackets []. Inside the outer brackets, there are two inner
square brackets [], each containing two elements. The memory view of a 2-d list is like a gird only and
to access any item from a 2-d list we need to pass two indexes, one for row and other for column. See
the next tabular view to understand the indexing of a 2-d list.
0 1
1 2
0
[0][0] [0][1]
3 4
1
[1][0] [1][1]
In the above grid numbers on top row and left-most column represents column and row indexes.
Numbers inside the square ‘[]’ brackets, such as [0][0] represents the location of the first item. To
access of these items, we need to combine row and column indices with the list name, as follows:
>>>A[0][0]
1
>>>A[1][0]
3
156
Python Fundamentals: A Functional Approach
If a 2-d list is small in size then it is easy to be declared with values. But if we want populate a 2-d list
based on user input, we need to do it row wise and keep appending it to the main 2-d list. Here is an
example of taking input from user of 3×3 matrix.
1 MAT = []
2 rows, cols = (3,3)
3 #Taking input rowwise
4 for i in range(0,rows):
5 row = input(f'Enter data:').split()
6 row = [int(item)for item in row]
7 MAT.append(row)
8
9 #print 2-d list in matrix form
10 for i in range(0,rows):
11 for j in range(0, cols):
12 print(MAT[i][j], end=' ')
13 print()
Enter data:1 2 3
Enter data:4 5 6
Enter data:7 8 9
1 2 3
4 5 6
7 8 9
157
Python Fundamentals: A Functional Approach
rows = 3
cols = 4
matrix = [[0 for _ in range(cols)] for _ in range(rows)]
The above illustration demonstrates the use of list comprehension for creating a 2D list filled with zeros.
Here's a breakdown:
▪ In the list comprehension, outer for _ in range(rows) loop iterates three times (0, 1, 2)
to create three rows.
▪ Within the outer loop, the inner [0 for _ in range(cols)] list comprehension creates a
sub-list (representing a row) filled with zeros.
▪ The inner loop iterates four times (0, 1, 2, 3) to create four zeros for each row.
▪ The entire expression creates a nested structure, building the 2D list with rows containing
zeros.
7.2 Tuple
Tuple is one of the built-in data types in Python used to store collections of data like list, set and
dictionary. A is an immutable ordered sequence of items, means existing objects in the sequence cannot
be modified, and new elements cannot be added once the sequence is created. Two ways we can create
a tuple, one using tuple() constructor method and other using a pair of empty parentheses “()”, as
shown below:
>>>t = ()
>>>type(t)
<class 'tuple'>
>>>t = tuple()
>>>type(t)
<class 'tuple'>
A tuple consists of several values separated by commas. Tuple items are ordered, unchangeable, and
allow duplicate values. Tuple items are indexed like list and the first item is accessed using index [0].
A tuple object can iterate through a loop using for or while. We can also use range() function to
generate the index of every item, as shown below:
158
Python Fundamentals: A Functional Approach
1 if 'apple' in fruits:
2 print('Fruits has an apple')
Tuple also support slicing and indexing like string and list which includes negative indexing. Some of
the examples are listed below:
>>>fruits[::]
('apple', 'banana', 'cherry')
>>>fruits[1:3]
('banana', 'cherry')
>>>fruits[::-1]
('cherry', 'banana', 'apple')
Trying to updating an item of tuple will raise a TypeError, as tuple object doesn’t supports item
assignment.
>>>fruits[0]='mango'
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
fruits[0]='mango'
TypeError: 'tuple' object does not support item assignment
To create a tuple with only one item, you have to add a comma after the item, otherwise Python will not
recognize it as a tuple.
>>>fruits = ('banana',)
>>>fruits
('banana',)
If comma is omitted Python won’t recognise that object as a tuple instead considered as string class
object.
>>>fruits = ('banana')
>>>type(fruits)
<class 'str'>
159
Python Fundamentals: A Functional Approach
modified after creation. This means you can't change the values of individual elements within a tuple.
But there is a workaround and you can convert the tuple into a list, change the list, and convert the list
back into a tuple.
Since tuples are immutable, directly you can’t add an item to it. But there are other ways to add
items to a tuple, by converting a tuple to list we can add item to it and later convert it to a tuple,
as follows:
1 fruits = ("apple", "banana", "cherry")
2 l1 = list(fruits)
3 l1.append("orange")
4 fruits = tuple(l1)
5 print(fruits)
Another way we can do by adding a tuple to another tuple instead of converting to a list.
>>>t1 = (2,3)
>>>t2 = (4,)
>>>t1 = t1+t2
>>>print(t1)
(2, 3, 4)
7.2.3 Methods
Packing and unpacking tuples in Python are convenient ways to work with multiple values.
Here's a breakdown of both concepts:
Packing refers to creating a tuple by assigning multiple values within parentheses () and the
order you specify the values becomes the order they are stored within the tuple.
>>>a, b, c = 1, 2, 3
>>>t = (a,b,c) #Packing to an object
>>>type(t)
<class 'tuple'>
In this example t become a tuple containing three integers 1,2,3. While Unpacking assigns individual
values from a tuple to separate variables. The number of variables must match the number of elements
in the tuple.
>>>a1, b1, c1 = t #unpacking an object
>>>print(a1, b1, c1)
1 2 3
You can unpack tuples with more or less elements than variables using extended unpacking with an
asterisk ‘*’. Unpacking with * before a variable name captures all remaining elements in a list.
>>> fruits = ("apple", "banana", "cherry", "mango", "orange")
>>> first_fruit, second_fruit, *other_fruits = fruits
>>> print(first_fruit)
apple
>>> print(second_fruit)
banana
>>> print(other_fruits)
['cherry', 'mango', 'orange']
▪ Immutable Data: Tuples are immutable, meaning their elements cannot be modified after
creation. This immutability makes tuples suitable for use cases where data should not change.
▪ Hashable: Tuples are hashable, which means they can be used as keys in dictionaries, while
lists cannot.
Here are some common use cases when tuples are preferable:
161
Python Fundamentals: A Functional Approach
▪ Return Multiple Values from Functions: Tuples are often used to return multiple values
from a function when you want to ensure that the values are treated as a single unit, discussed
in Chapter 8.
▪ Unpacking: Tuples can be used for unpacking values returned from functions or when
iterating through sequences, just discussed in Section 7.2.4.
▪ Dictionary Keys: Since tuples are hashable, they can be used as keys in dictionaries when the
key is a combination of multiple values, discussed more in details later in this Chapter.
>>> coordinates = {(1, 2, 3): "Point A", (4, 5, 6): "Point B"}
>>> coordinates[(1,2,3)]
'Point A'
▪ Records or Structs: Tuples can represent a fixed collection of related data fields, similar to a
lightweight struct or record.
▪ Immutability: When you need to ensure that data remains constant throughout the program,
using tuples can prevent accidental modifications.
▪ Performance: Tuples can be slightly faster than lists when it comes to iteration and indexing
since they are immutable and have a fixed structure.
▪ Data Integrity: Tuples can be used to enforce data integrity by preventing the accidental
modification of values within a collection.
▪ Function Arguments: Tuples can be used to pass multiple arguments to functions without
explicitly defining named parameters. Discussed in Chapter 8.
Remember that the choice between tuples, lists, and other data structures depends on the specific
requirements of your program and the nature of the data you are working with. Tuples are particularly
useful when you need a fixed, unchangeable collection of items.
In Python, there is no direct tuple comprehension construct like there is for list comprehensions.
However, you can create a tuple using a generator expression and the tuple() constructor to convert
it into a tuple. A generator expression is similar to a list comprehension, but it returns an iterable
generator object instead of a list. Here's the basic structure of a tuple comprehension using a generator
expression:
tuple_variable = tuple(expression for element in iterable)
The meaning of each component in the above syntax is summarised below:
▪ expression: The expression to evaluate for each element in the iterable.
▪ element: The variable that takes on each value from the iterable.
▪ iterable: The iterable you want to loop through to create the tuple.
Here's an example of creating a tuple using a generator expression:
>>>numbers = [1, 2, 3, 4, 5]
>>>squared_tuple = tuple(x ** 2 for x in numbers)
>>>print(squared_tuple)
(1, 4, 9, 16, 25)
162
Python Fundamentals: A Functional Approach
7.3 Set
In Python, a set is a built-in data structure used to store a collection of unique and unordered elements.
Items of the set are immutable type objects and sets are defined by enclosing a comma-separated
sequence of elements within curly braces {} or by using the built-in set() constructor method. Once a
set is created set items are not modifiable, but you can add new items using .add(elem) method. Let
us first create a list by using comma-separated sequence of elements:
>>>my_set = {1, 2, 3, 4, 5}
>>>my_set
{1, 2, 3, 4, 5}
Note that you should not use empty curly braces to create an empty set because Python will interpret it
as a dictionary.
>>>s = {}
>>>type(s)
<class 'dict'>
The set() constructor allows you to create an empty set. Items can be added later using the add()
method. As shown in the next illustration
>>>s = set()
>>>type(s)
<class 'set'>
>>>s.add(1)
>>>s.add(2)
>>>s
{1, 2}
Remember adding an item is not ordered in a set like a list. It can be placed anywhere of a given object
and like list, set doesn’t keep duplicate items. Python simply ignored the item and keep the first item.
Now a duplicate item to set s, that we have created in the previous illustration.
While creating a set using set() constructor method, you can pass an iterable (e.g., a list or tuple) object
as an argument. This will create a set with the unique elements from the iterable. For example:
>>>my_list = [1, 2, 2, 3, 4, 4, 5]
>>>my_set = set(my_list)#duplicate items rejected
>>>my_set
{1, 2, 3, 4, 5}
163
Python Fundamentals: A Functional Approach
Similar to the list comprehensions, you can use set comprehensions to create sets by applying an
expression to each element of an iterable. For example:
>>>my_list = [1, 2, 3, 4, 5]
>>>my_set = {x * 2 for x in my_list}
>>>my_set
{2, 4, 6, 8, 10}
Like list and tuple using in and not in operators can be used to test the membership of any item and
len() function returns the number of elements in a set:
>>>len(my_set)
5
>>>2 in my_set
True
>>>11 in my_set
False
Sets items are not accessible using index like list or tuple objects. Sets items are iterate through loop
using for loop as follows:
>>> s = {1,2,3}
>>> for item in s:
... print(item, end=' ')
...
1 2 3
Union of two sets: Set operations in Python can be performed in two different ways: by operator or
by method. Let’s take a look at how these operators and methods work, using set union as an example.
Given two sets, set1 and set2, the union of set1 and set2 is a set consisting of all elements in either
set. Consider these two sets:
>>>set1 = {1,3,4,5,6}
>>>set2 = {1,7,3,10}
In Python, set union can be performed with the union (|) operator:
>>>set1 | set2
{1, 3, 4, 5, 6, 7, 10}
Note that elements 1 and 3, which appears in both the sets, appears only once in the union operation.
Set union can also be obtained with the union() method. The method is invoked on one of the sets,
and the other is passed as an argument:
>>>set1.union(ste2)
164
Python Fundamentals: A Functional Approach
{1, 3, 4, 5, 6, 7, 10}
The working of union operator (|) and union() method is quite different. For union operator |, both
the operands must be of set type, but union() method can accept any iterable type as argument, later
converts that iterable as set and perform union operation. See the next illustration:
>>>num1 = {1,3,4,5,6}
>>>num2 = tuple([11,2,3])
>>>num1|num2
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
num1|num2
TypeError: unsupported operand type(s) for |: 'set' and 'tuple'
>>>num1.union(num2)
{1, 2, 3, 4, 5, 6, 11}
Python raised TypeError if any of the operand is a different type of object, other than set. But union()
can take any type of iterable object as argument and perform the union operation. More than two sets
may be specified with either the operator or the method:
>>>a = {1, 2, 3, 4}
>>>b = {2, 3, 4, 5}
>>>c = {3, 4, 5, 6}
>>>a.union(b, c)
{1, 2, 3, 4, 5, 6}
>>>a | b | c
{1, 2, 3, 4, 5, 6}
Intersection between two sets: a.intersection(b) and a & b return the set of elements common
in both sets a and b. You can also specify multiple sets with the intersection method or operator, just
like you can with set union operation. If there are no common elements between the sets, the
intersection will be an empty set set().
>>>a.intersection(b)
{2, 3, 4}
>>>a & b
{2, 3, 4}
>>> c = {6,7}
>>> a & c
>>> a & c #returns an empty set
set()
Like set union method and operator, the intersection() method and intersection (&) operator can
handle intersections of more than two sets at once.
Difference between two sets: Set difference finds elements that are in the first set but not in the
second. a.difference(b) and a-b is the required syntax to get the set of all elements that are in a but
not in b:
>>>a.difference(b)
165
Python Fundamentals: A Functional Approach
{1}
>>>a-b
{1}
Another way to think of this is that a.difference(b) and a-b return the set, that results when elements
are present in a not in b. In simpler terms it removes the “overlap” between a and b. Once again, you
can specify more than two sets, when multiple sets are specified, the operation is performed from left
to right. In the example below, a - b is computed first, resulting in {1, 2, 3, 300}. Then c is subtracted
from that set, leaving {1, 2, 3}.
>>>a = {1, 2, 3, 30, 300}
>>>b = {10, 20, 30, 40}
>>>c = {100, 200, 300, 400}
>>>a-b-c
{1, 2, 3}
Symmetric Difference: In Python, a.symmetric_difference(b) and a^b are two ways to find the
symmetric difference between two sets a and b. The symmetric difference of two sets contains all
elements that are in either of the sets but not in their intersection. In other words, it includes elements
that are unique to each set.
The symmetric difference can also be performed using difference operator between the union and
intersection operations of two sets as follows:
>>>(a | b) - (a & b)
{1, 2, 3, 40, 10, 300, 20}
Like other set operators’ symmetric difference (^) operator also allowed with multiple sets to perform
symmetric difference. But symmetric_difference() method exactly allows only one argument.
Disjoint of two sets: Using a.isdisjoint(b) method we can determine a and b are disjoint set or
not. Two sets are disjoint, if they have no elements in common, and if a.isdisjoint(b) is True, then
a & b returns an empty set
Subset of a set: In set theory, a set a is considered a subset of another set b, if every element of a is
in b. The required logic for subset operation is shown below:
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
flag = True
#For subset length of set a should be less than or equal to b
if len(a)>len(b):
print('a is not a subset of b')
else:
#iterating over each item of a
for item in a:
#checking membership in set b
if item not in b:
#update the flag and break from loop
flag = False
break
if flag:
print('a is subset of b')
else:
print('a is not a subset of b')
Python offers a method issubset() and a subset operator (<=) to check a set is subset of another or
not.
>>>a.issubset(b)
True
>>>a<=b
True
Proper subset: The operation a<b, determines whether one set is a proper subset of the other. A proper
subset is the same as subset, except that the sets can’t be identical. A set a is considered a proper subset
of b, if every element of a is in b, and a and b are not equal.
>>>a = {1,2}
>>>b = {1,2,3}
>>>a<b
True
The < operator is the only way to test whether a set is a proper subset or not. There is no corresponding
method.
Superset: Superset operator (>=) is used to check a set is superset of another or not and the
corresponding method is issuperset(). A set b is considered a superset of another set a, if b contains
all the element of a.
>>>b>=a
True
>>>b.issuperset(a)
167
Python Fundamentals: A Functional Approach
True
a>b, determines whether one set is a proper superset of the other. A set a is considered a proper
superset of another set b, if b contains every element of a, and a and b are not equal.
>>>b>a
True
>>>a.add(3) #both sets are equal
>>>b>a
False
The proper superset (>) operator is the only way to test whether a set is a proper superset or not, there
is no corresponding method.
a.remove(<elem>) removes an <elem> from set a, if <elem> is not present, Python will raises an
KeyError exception:
>>>a = {1,2,3,4,5}
>>>a.remove(1)
>>>a
{2, 3, 4, 5}
If you don’t want to handle any exception while removing an element from a set, then use
x.discard(<elem>), instead of a.remove(<elem>). This method quietly does nothing if <elem> is not
in set a.
>>>a.discard(10)
>>>a
{2, 3, 4, 5}
>>>a.discard(2)
>>>a
{3, 4, 5}
a.pop() removes and returns an arbitrarily chosen element from set a. If set a is empty, a.pop() raises
an exception:
Instead of removing items one by one clear() methods removes all elements from a set. A set can be
updated by adding items from other set using update() method or |= operator.
>>> b
{2, 3, 4, 5}
>>> b.update({6,7})
168
Python Fundamentals: A Functional Approach
>>> b
{2, 3, 4, 5, 6, 7}
An in-place update of a set can be done using intersection_update(other_set) method or &=
operator, that modifies a set by keeping only the elements common to both sets.
>>> a = {1,2,3}
>>> b = {2, 3, 4, 5, 6, 7}
>>> id(a)
2309581386496
>>> a.intersection_update(b)
>>> a
{2, 3}
>>> id(a)
2309581386496
An in-place modification changes the values or elements within an existing data structure (like a list, set,
or object). The original data structure is mutated, and it reflects the changes made. In the above
illustration id() on a, before and after modification returns same memory reference. Like
intersection_update(other_set), difference_update(other_set) method or -= operator
modifies a set by removing elements found in the other set as in-place modification.
>>> a = {1,2,3}
>>> b
{2, 3, 4, 5, 6, 7}
>>> a.difference_update(b)
>>> a
{1}
The last method of this type is symmetric_difference_update(other_set) method or ^= operator,
modifies the set by keeping elements that are in exactly one of the sets.
>>> a = {1,2,3}
>>> a.symmetric_difference_update(b)
>>> a
{1, 4, 5, 6, 7}
Python provides another built-in type called a frozenset, which is in all respects exactly like a set, except
that a frozenset is immutable. You can perform non-modifying operations on a frozenset. The inbuilt
function called frozenset() that takes an iterable object as input and makes it immutable. Simply put, the
Python Frozenset renders iterable objects unchangeable by freezing them.
169
Python Fundamentals: A Functional Approach
frozenset() takes only one parameter, that contains the elements that will be used to initialize the
frozenset and returns a frozenset with items from the supplied iterable that is untouched. It returns an
empty frozenset if arguments are not given.
You can perform set operations such as union, intersection, and difference with other frozensets, sets,
or even other iterable types (converted to sets or frozensets).
7.5 Dictionary
Python provides another composite data type called a dictionary, which is similar to a list in that it is a
collection of objects. Dictionaries and lists share the following characteristics:
170
Python Fundamentals: A Functional Approach
▪ List elements are accessed by their position in the list, via indexing.
▪ Dictionary elements are accessed via keys.
Dictionaries are Python’s implementation of a data structure that is more generally known as an
associative array. A dictionary consists of a collection of key-value pairs. Each key-value pair maps the
key to its associated value. You can define a dictionary by enclosing a comma-separated list of key-value
pairs in curly braces ({}). A colon (:) separates each key from its associated value:
records = {'name':'Goutam',
'age':37,
'city':'Vellore',
'Country':'India'
}
You can also construct a dictionary with the built-in dict() function. The argument to dict() should
be a sequence of key-value pairs. A list of tuples works well for this:
If the key values are simple strings, they can be specified as keyword arguments. So here is yet another
way to define records:
The entries in the dictionary display in the order they were defined. But that is irrelevant when it comes
to retrieving them. Dictionary elements are not accessed by numerical index like list. It will raise a
KeyError, as shown below:
>>>records
{'name': 'Goutam', 'age': 37, 'city': 'Vellore', 'Country': 'India'}
>>>records[0]
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
records[0]
KeyError: 0
171
Python Fundamentals: A Functional Approach
Defining a dictionary using curly braces and a list of key-value pairs, as shown above, is fine if you know
all the keys and values in advance. But what if you want to build a dictionary on the fly? That you can
start by creating an empty dictionary, which is specified by empty curly braces ( {}). Then you can add
new keys and values one at a time.
>>>person = {}
>>>type(person)
<class 'dict'>
Once an empty dictionary is created then many ways key-value pairs can be added to the dictionary. One
of the methods is by using square brackets and the assignment operator (=). If the key does not exist, it
will be added; if it does exist, the value associated with that key will be updated.
>>>person['fname'] = 'Joe'
>>>person['lname'] = 'Fonebone'
>>>person['age'] = 51
>>>person['pets'] = {'dog': 'Fido', 'cat': 'Sox'}
>>>person
{'fname': 'Joe', 'lname': 'Fonebone', 'age': 51, 'pets': {'dog': 'Fido',
'cat': 'Sox'}}
You can use the update() method to add multiple key-value pairs from another dictionary or an
iterable of key-value pairs.
>>>my_dict = {}
>>>my_dict.update({'name': 'John', 'age': 30})
>>>my_dict
{'name': 'John', 'age': 30}
You can build a dictionary incrementally by iterating through an iterable and adding key-value pairs
based on the elements of the iterable. See the illustration given below:
1 my_dict = {}
2 data = [('name', 'John'), ('age', 30)]
3 for key, value in data:
4 my_dict[key] = value
5
6 print(my_dict)
You can create a dictionary incrementally using a dictionary comprehension, which allows you to
transform one or more iterables into a dictionary.
172
Python Fundamentals: A Functional Approach
You can define a function that adds key-value pairs to a dictionary and call that function to build the
dictionary incrementally. More about def is discussed in Chapter 8.
A value is retrieved from a dictionary by specifying its corresponding key inside a pair of square brackets
([]):
>>>records['name']
'Goutam'
Adding an entry to an existing dictionary is simply a matter of assigning a new key and value using
assignment operator:
>>>records['height'] = 5.56
>>>records
{'name': 'Goutam', 'age': 37, 'city': 'Vellore', 'Country': 'India',
'height': 5.56}
If you want to update an entry, you can just assign a new value to an existing key:
>>>records['city'] = 'Agartala'
>>>records
{'name': 'Goutam', 'age': 37, 'city': 'Agartala', 'Country': 'India',
'height': 5.56}
Retrieving the values in the sublist or subdictionary requires an additional index or key. Consider the
person dictionary, which has children and pets as keys and values are of type list and dictionary.
173
Python Fundamentals: A Functional Approach
This example exhibits another feature of dictionaries: the values contained in the dictionary don’t need
to be the same type. In person, some of the values are strings, one is an integer, one is a list, and one
is another dictionary. Just as the values in a dictionary don’t need to be of the same type, the keys don’t
either. See the next example, illustrates a dictionary with different types of keys:
To delete an entry, use the del statement, specifying the key to delete:
>>>del records['height']
>>>records
{'name': 'Goutam', 'age': 37, 'city': 'Agartala', 'Country': 'India'}
In Python, dictionary keys can be of any hashable data type. A hashable data type is one that is both
immutable and has a hash value that never changes during its lifetime. This is because dictionaries use a
hash table to store their key-value pairs, and keys must be hashable to ensure efficient lookup and
retrieval. Here are some common data types that can be used as dictionary keys:
▪ Numeric types: numeric data types such as integers, floating-point numbers, and complex numbers
are generally considered hashable because they are immutable. As long as the value of a numeric
data type remains constant, its hash value will also remain the same. Here are a few examples of
numeric data types as dictionary keys:
# Integers as keys
int_dict = {1: "one", 2: "two", 3: "three"}
# Floats as keys
float_dict = {3.14: "pi", 2.71828: "e"}
# Complex numbers as keys
complex_dict = {1 + 2j: "complex_number_1", 3 - 4j: "complex_number_2"}
▪ We have already seen some examples of frozenset and string as dictionary key.
▪ You can even use built-in objects like types and functions as key:
>>>d = {int: 1, float: 2, bool: 3}
>>>d
{<class 'int'>: 1, <class 'float'>: 2, <class 'bool'>: 3}
174
Python Fundamentals: A Functional Approach
>>>d[int]
1
▪ Custom Objects: You can use custom objects as dictionary keys if you implement the __hash__()
and __eq__() methods to define their hash value and equality.
▪ It's important to note that mutable objects, such as lists and dictionaries, cannot be used as
dictionary keys because they are not hashable, and their content can change, leading to unpredictable
behavior. Using an unhashable object as a key will result in a TypeError.
d = {[1,2]:12}
Instead of adding ‘Minnesota’: ‘Timberwolves’ to the dictionary, it updates the associated value.
Similarly, if you specify a key second time during the initial creation of a dictionary, the second
occurrence will override the first:
A dictionary key must be of a type that is immutable. You have already seen examples where several of
the immutable types you are familiar with—integer, float, string, and Boolean—have served as dictionary
keys. A tuple can also be a dictionary key, because tuples are immutable. However, neither a list nor
175
Python Fundamentals: A Functional Approach
another dictionary can serve as a dictionary key, because lists and dictionaries are mutable:
>>>hash('foo')
6821459173919347162
>>>hash([1, 2, 3])
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
hash([1, 2, 3])
TypeError: unhashable type: 'list'
All of the built-in immutable types you have learned about so far are hashable, and the mutable container
types (lists and dictionaries) are not. So, for key purposes, you can think of hashable and immutable as
more or less synonymous.
Dictionary come with a variety of built-in methods to manipulate and work with the data stored in
them. Here are some common dictionary methods:
▪ dict.keys(): Returns a view of all the keys in the dictionary as a type of dict_keys. An object of
type dict_keys can be passed inside a list() to convert it a list of keys. See the illustration shown
below:
>>>d = {'a': 10, 'b': 20, 'c': 30}
>>>d.keys()
dict_keys(['a', 'b', 'c'])
>>>list(d.keys())
['a', 'b', 'c']
This method can be also used in a for loop to iterate over a dictionary and get the associated value with
respective key:
176
Python Fundamentals: A Functional Approach
▪ dict.values(): Returns a view of all the values in the dictionary as a type of dict_values and can
be passed inside built-in list() method to get all the values as a list.
>>> d.values()
dict_values([10, 20, 30])
▪ dict.items(): Returns a view of all key-value pairs (tuples) in the dictionary. Instead of using
keys() and values(), if we need both then items() can be used in a for loop to iterate key and value
pair.
>>> d = {'a': 10, 'b': 20, 'c': 30}
>>> for key, value in d.items():
... print(f'{key}:{value}',end=' ')
...
a:10 b:20 c:30
▪ dict.get(key, default) : Returns the value for the specified key, or a default value if the key
is not found.
>>>my_dict = {'name': 'John', 'age': 30}
>>>my_dict.get('name', 'Unknown') #returns John
'John'
>>>my_dict.get('country', 'USA') #returns default value
'USA'
The arbitrariness of the removed pair means that you cannot predict which key-value pair popitem()
will return. If you need to remove the last item from a dictionary, you can use the pop() method by
specifying the key you want to remove. Here's an example:
>>>d.pop('c')
177
Python Fundamentals: A Functional Approach
30
>>>d
{'a': 10, 'b': 20}
▪ dict.clear(): Removes all key-value pairs from the dictionary, leaving it empty.
▪ dict.copy(): Returns a shallow copy of the dictionary. A shallow copy of a dictionary is a new
dictionary that contains references to the same objects (key-value pairs) as the original dictionary,
rather than creating copies of the objects themselves. Consider the dictionary given below, which
contains nested dictionary as value of original_dict:
original_dict = {
'name': 'John',
'info': {
'age': 30,
'address': {
'city': 'New York',
'state': 'NY'
}
}
}
In the above declaration, a dictionary info is nested inside original_dict and address is nested
inside info. Creating a shallow copy of it, means that changes made to the objects within the dictionary
will be reflected in both the original and the shallow copy, as they still reference the same objects.
178
Python Fundamentals: A Functional Approach
If you don’t want to modify the orginal_dict, while modifying the copied dictionary, then you need
to produce a deeper copy of the original_dict by using deepcopy() method of copy module. An
example shown below:
Before illustration of the above code, we have redefined the original_dict with previous key-value
pairs. The recursive part of deep copying involves copying not only the top-level objects but also delving
into nested objects and copying them as well. This process continues until all nested objects are
duplicated, creating a complete independent copy of the entire structure. In contrast, a shallow copy of
the same compound object would only copy references to the nested objects, meaning changes in the
nested objects would still be reflected in both the original and the shallow copy.
▪ dict.fromkeys(keys, value): This method is used to creates a new dictionary from keys and
an optional value passed as parameter. The keys must be an iterable object such as list, tuple or
string and if you don't provide a value in the second argument, all the keys will be assigned None
by default.
>>> keys = ['name', 'age', 'country']
>>> new_dict = dict.fromkeys(keys)
>>> new_dict
{'name': None, 'age': None, 'country': None}
▪ dict.setdefault(): method is used with dictionaries to add a new key-value pair to an existing
dictionary object, if the key does not exist. Else it will return the associated value with if key exist.
This method is particularly useful when you want to ensure that a key is present in a dictionary and
provide a default value if it's not.
>>> my_dict = {'name': 'John', 'age': 30}
>>> my_dict.setdefault('country', 'USA')
'USA'
>>> my_dict
{'name': 'John', 'age': 30, 'country': 'USA'}
>>> my_dict.setdefault('country', 'India')
'USA'
In the above illustration, the key-value pair (‘country : USA’) is added to my_dict, and on trying to
adding a same key, the method returns the existing associated value with it. If you didn’t pass the
associated value, a None value will be added with the key. In the example listed below, the 'city' key
179
Python Fundamentals: A Functional Approach
did not exist, so setdefault() adds the key with a value of None.
Values of one dictionary can updated using the key:value pair from another dictionary using update()
method.
You can also use the keyword arguments syntax to pass key-value pairs directly to the update method.
Key Takeaway’s
▪ Lists are created by enclosing a comma-separated sequence of elements within square brackets [].
▪ Be cautious about aliasing (two variables referring to the same list), which can lead to unintended
side effects.
▪ Use list.copy() or slicing [:] to create a shallow copy of a list.
▪ You can clear a list by assigning an empty list to it: my_list = []
▪ List supports common operations like adding, removing, searching and counting elements.
▪ Lists can be memory-intensive when they contain many elements. In such cases, consider using
generators or other data structures.
180
Python Fundamentals: A Functional Approach
▪ Lists are suitable for storing collections of items when you need to maintain order, and you need
the ability to modify the collection during the program's execution
▪ Lists are indexed by integers, while dictionaries are indexed by keys.
▪ Lists allow duplicates and have an order, while sets do not allow duplicates and have no order.
▪ Lists are like tuples but are mutable, whereas tuples are immutable.
▪ Sets are created using curly braces {} or the set() constructor method. But a pair of empty curly
braces {} creates a dictionary, instead of a set.
▪ Sets are mutable, meaning you can add and remove elements.
▪ Sets do not support indexing, slicing, or other sequence operations because they are unordered.
▪ Sets are commonly used for eliminating duplicates and performing mathematical set operations.
▪ A frozenset is an immutable version of a set and created using the frozenset() constructor
method.
▪ Frozensets are hashable and can be used as keys in dictionaries, unlike sets.
▪ Frozensets are useful when you need an immutable collection of unique elements and when you
want to use them as keys in dictionaries.
▪ A dictionary is an unordered collection of key-value pairs are created using curly braces {} or the
dict() constructor method.
▪ Dictionary keys must be unique, and they are case-sensitive.
▪ Values in a dictionary do not need to be unique, and the same value can be associated with multiple
keys.
▪ In Python versions prior to 3.7, dictionaries were unordered, meaning the order of key-value pairs
was not guaranteed. In Python 3.7 and later, dictionaries maintain insertion order.
▪ Dictionaries are used to store and retrieve data efficiently, and they are suitable for situations where
you need to associate keys with values.
Practice Questions
1. Analyse a stock portfolio by storing stock symbols, quantities, and prices in tuples, and then calculate
total portfolio value, average price, maximum stock price, minimum stock price, total stock. Consider
the given portfolio:
portfolio = [("AAPL", 10, 150.0), ("GOOGL", 5, 2700.0),
("TSLA", 8, 700.0),
("AMZN", 4, 3500.0),
]
2. Take a list of numbers as input from user and finds the sum and product of all the numbers.
3. Take a list of numbers as input from user and find the maximum and minimum values and print
them without using min() and max() function.
4. Write a program to manage student records with attributes like name, age, and CGPA. Store each
student's information as a tuple in a list and display the student record with highest CGPA.
5. Take a list of numbers as input and calculates mean, median, mode for a list of numbers.
Hint: The median of a list of numbers is the "middle" value when the list is ordered from least to
greatest. It represents the point where half the numbers in the list are smaller, and the other half are
larger. For odd number of items, the median is the middle number and for even number of items
181
Python Fundamentals: A Functional Approach
median is the average of the two middle numbers. The mean of a list of numbers, also called the
average.
The mode of a list of numbers is the number that appears most frequently within the list. There can
be one mode, multiple modes, or even no mode at all in a given list.
6. Create a program that sums up even numbers from a given list of integers. Use continue to skip odd
numbers and break if the sum reaches 100 or more.
7. Given a list of names, write a program that continues to ask the user for a name until it finds a match
in the list. If a match is found, break the loop and print the position of the name in the list.
8. Write a program to take a string as input and form a dictionary, that will have every character as key
and frequency count of each character will be the value. See the sample input output.
Input: "Mississippi"
Output: {'M': 1, 'i': 4, 's': 4, 'p': 2}
9. Write a program to take a string with repeated characters as input and compress the string by
converting repeated characters into tuples containing the character and the number of repetitions as
a list (e.g., "aaabbbbcc" becomes [('a', 3), ('b', 4), ('c', 2)]).
10. Create a dictionary containing the information about subject’s code and number. Subject codes will
be the key and score will be the associated value. See the dictionary as given below and perform the
operations as listed from (a) – (f).
subjects = {"MATH101": 85, "PHYS202": 92, "CS303": 78}
(a) Add the details of two other subjects as “CSE202”: 67, “INT304”: 56.
(b) Check a subject code already exists or not, if already there, then display a message ‘Already
Added’, else add the new subject.
(c) Display the subject name that have highest marks
(d) Calculate the average and total marks
(e) Design the functionality to remove any subject from the dictionary
11. Write down a program and take two lists of integers from user as input and find the followings:
(a) Prepare a third list which have all the unique numbers after removing the duplicate items
(b) Prepare a third list having elements that are present in both lists.
(c) Prepare a list that will have the elements present in the first list but not in the second
(d) Prepare a list that have elements that are in either list but not in both.
(e) Print True, if the list has no common elements, else False.
12. Prepare a python script that will take two sets as input from the user and display a dictionary
containing the following set operations:
(a) Union of set1 and set2.
(b) Intersection of set1 and set2.
(c) Difference of set1 minus set2.
(d) Symmetric difference of set1 and set2.
The dictionary should have keys "union", "intersection", "difference", and "symmetric_difference".
13. Write a Python script that takes two dictionaries as input and returns a new dictionary containing all
the key-value pairs from both dictionaries. If there are overlapping keys, the values from dict2 should
overwrite those from dict1.
182
Python Fundamentals: A Functional Approach
14. Create a python script that takes a dictionary as input and returns a new dictionary where the keys and
values are swapped. The input dictionary should be assumed to have unique values.
****************END OF CHAPTER****************
183
8 FUNCTIONS
You may be familiar with the mathematical concept of a function. A function is a relationship or
mapping between one or more inputs and a set of outputs. In mathematics, a function is typically
represented like this:
𝑧 = 𝑓(𝑥, 𝑦)
Here 𝑓 is a function that operates on the inputs 𝑥 and 𝑦. The output of the function is 𝑧. However,
programming functions are much more generalized and versatile than this mathematical definition. In
fact, appropriate function definition and use is so critical to proper software development that virtually
all modern programming languages support both built-in and user-defined functions.
In programming, a function is a self-contained block of code that encapsulates a specific task or
related group of tasks. Previously we have been introduced to some of the built-in functions provided
by Python such as len() ,id(). Both these functions take one argument and 𝑙𝑒𝑛( ) returns the
length of the object and id() returns the object’s unique integer identifier.
The len() function returns the number of items in a given sequence. At first, we have passed msg as
input to len(), returns the number of characters in “Hello, All!”. Later a list is passed as an
argument and it returns the number of items in the list numbers and id() returns the unique
identification number of the specified object.
>>> id(msg)
1815290128560
>>> id(numbers)
1815290131776
Each of these built-in functions performs a specific task. The code that performs the specified task
is defined somewhere, but we don’t need to think about the working of the code. Only we need to know
about the interface of the function which may or may not have the following: (a) one or many arguments,
it takes as input and (b) one or many values it returns.
184
Python Fundamentals: A Functional Approach
In the examples you have seen that, we have passed the appropriate arguments to id() and len().
After the successful execution of a function call both functions return a value. Once we call a function,
program execution goes off to the designated body of the code and does its useful thing. When the
function body is finished, program execution returns to the code from where it was left off. Let see the
pictorial view of a function call:
In Fig. 8.1, we have depicted a flow of program execution and shown in a sequence of numbers from 1
– 6. The box written with Function caption represents a definition of the function and program
execution starts from sequence number 1. At this point your script file can have any valid python
statements, such as declaration or assignment. Thereafter at sequence number 2 the function calls get
executed. Due to this, the program control has jumped (mentioned as number 3) inside the function, to
execute the line of statements.
After the successful execution of the function, program control returns to the same point
(represented as sequence number 2) of the script, from where the control was jumped. While the
program control is returning, it can have some return value as well. Thereafter, program execution will
continue till the execution of the last line of code. When you define your own Python function, it works
just the same. From somewhere in your code, you’ll call your Python function and program execution
will transfer to the body of the code that makes up the function.
When the function is finished, execution returns to the location where the function was called.
Depending on how you designed the function’s interface, data may be passed in when the function is
got called and return values may be passed back when it finishes.
Virtually all programming languages support a form of user-defined functions, although they aren’t
always called functions. In other languages, you may see them referred to as, (a) subroutines; (b)
185
Python Fundamentals: A Functional Approach
procedures; (c) methods; and (d) subprograms. So, why bother defining functions? There are several
very good reasons. Let’s go over a few now.
8.1.2 Modularity
Functions allow complex processes to be broken up into smaller steps. Imagine, for example, that you
have a program that reads in a file, processes the file contents, and then writes an output file. Your code
could be look like this:
In Fig. 8.2, we have listed three different segments under the main program and have put all the code
segments together in a long sequence. Further, if we consider the whitespace and comments to help
186
Python Fundamentals: A Functional Approach
organize the code, the length of the script will increase. However, if the code were to get much lengthier,
it would become very difficult to manage the code. Instead of this we can use the modular approach to
separate the code segments using functions and later we can call the functions accordingly. A simple
modular approach of python script is shown in Fig. 8.3.
Instead of all the code being put together, it’s broken out into separate functions, each of which
focuses on a specific task. Those tasks are reading, processing, and writing. The main program now
simply needs to call each of these in turn. Breaking a large task into smaller, bite-sized sub-tasks helps
make the large task easier to think about and manage. As programs become more complicated, it
becomes increasingly beneficial to modularize them in this way.
187
Python Fundamentals: A Functional Approach
The statement(s), is called the body of the function. The body is a block of statements that will
be executed when the function is called. The body of a function is defined by indentation just like other
control structures such as: an if or while statement. Once the function definition is completed, we
need to call the function so that code associated within the function gets executed. Consider the syntax
given below to call a function:
>>> function_name(arguments)
arguments are the values passed into the function. They correspond to the parameters in the function
definition. You can define a function that doesn’t take any arguments, but the parentheses are still
required after a function name either in definition or a function call, even if they’re empty. Let us start
with a basic example to understand the basic concept of python function:
1 def fun():
2 msg = 'inside fun'
3 print(msg)
4
5 print('Start of Program')
6 fun()
7 print('End of Program')
Start of Program
inside fun
End of Program
188
Python Fundamentals: A Functional Approach
▪ Line 4: a whitespace between the function definition and the first line of the main program just
improves the code readability. This whitespace segregates the function definition from the main
script.
▪ Line 5: is the first statement that isn’t indented because it isn’t a part of the definition of fun().
It’s the start of the main program and when the executes, this statement is executed first.
▪ Line 6: is a call to fun(). Note that empty parentheses are always required in both a function
definition and a function call, even when there are no parameters or arguments. Execution
proceeds to fun() and then statements in the body of fun() are executed. A sequence of
program execution is depicted in Figure 8.1.
▪ Line 7: is the next line to execute once the body of fun() has finished. Execution returns to
this print() statement.
Start of Program
End of Program
We can see that calling an empty function is syntactically correct but doesn’t add any value to the output.
6 fun('Tridib', 7)
7 fun('Goutam', 37)
A function fun(name, age) is defined with two parameters name and age. These parameters behave
like variables that are defined locally to the function. When the function is called fun(‘Tridib’, 7) at
line 6, are bound to the parameters in order, as though by variable assignment and ‘Rahul’ and 45 is
assigned to name and age in order. While calling a function following points need to take care:
▪ The number of parameters in the function definition should match the numbers of arguments
in the function call. Now let’s try to call the above function with one argument name, as follows:
>>>fun('Rahul')
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
fun('Rahul')
TypeError: fun() missing 1 required positional argument: 'age'
Interpreter raised a TypeError as one positional argument age is missing. Even though, if we pass
more number arguments than the required number, interpreter will raise a TypeError. See the
function call given below with the error:
>>>fun('Rahul', 25, 'Male')
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
fun('Rahul', 25, 'Male')
TypeError: fun() takes 2 positional arguments but 3 were given
▪ We need to take care of the order of arguments while calling a function. The order should match
the order of the parameters in the function definition. There’s nothing to stop you from
specifying positional arguments out of order, of course:
>>>fun(35,'Rahul')
35 is Rahul years old
The function may even still run, but due to change of order in the arguments we did not get the
desired output because 35 is assigned to name and Rahul is assigned to age. It’s the responsibility of
the programmer who defines the function to document what the appropriate arguments should be,
and it’s the responsibility of the user of the function to be aware of that information and abide by
it.
Positional arguments are conceptually straightforward to use, but they’re not very forgiving. You must
specify the same number of arguments in the function call as there are parameters in the definition, and
in the same order. Later in this chapter, you will find some argument-passing techniques that relax these
restrictions.
previously defined function fun() may be called with keyword arguments as follows:
>>>fun(name = 'Rahul', age = 45)
While we are calling the function with keyword order of parameter is not important anymore. Consider
the next function call given below:
While calling the function we have put a wrong keyword name that does not match the function
parameter. Instead of the given parameter name, we have used nam maybe knowingly or unknowingly
and thus keywords didn’t match during the function call and raised a TypeError. Like positional
arguments, keyword arguments must match the number of parameters, or else raise a TypeError.
In the function definition values 3 and 4 are assigned to the parameter ‘a’ and ‘b’. During the
function call, if we forget to pass value to any parameters, then the default value is considered. See the
various form of function call fun():
>>>fun()
>>>fun(10)
>>>fun(10, 20)
All these function calls are valid, and we will get the desired output. The first function call fun(),
doesn’t take any argument and the default values 3 and 4 is assumed for the parameters ‘a’ and ‘b’.
The second call fun(10), has one argument, and values are assigned according to the position in
191
Python Fundamentals: A Functional Approach
function definition from left to right. It means the default value for the parameter ‘a’ is overwritten by
10 and default value for the parameter ‘b’ will be retained. In the third call fun(10,20), we have
passed all the arguments and all the default values for the parameters are overwritten, means 10 and 20
is assigned to ‘a’ and ‘b’. The output of all the function calls is given below:
a = 3, b = 4
a = 10, b = 4
a = 10, b = 20
The second call fun(10) is assigned value 10 to the parameter a, what if, we want to assign the
passing argument to parameter ‘b’ not ‘a’. To achieve this, we can mention the keyword at the time
of function calling, provided all the parameters assigned with default value. See the function call given
below:
>>>fun(b=9)
a = 3, b = 9
In the above function call default value for ‘a’ is assumed and 9 is assigned to ‘b’. Now the question
is can we mix default parameters with non-default parameters in a function definition. Let’s us consider
the function definitions given below:
In both the functions fun1(), fun2() definitions we have assigned only one default parameter. In
fun1(), default argument follows non-default argument and in fun2(), non-default argument follows
default argument. Now let’s execute the above function and see if both the definitions are accepted by
Python or not.
On execution you will get a syntax error, specifying that “non-default argument follows default
argument” is pointed to the function definition fun2(). Means out of both the function definitions only
fun1() is valid in Python. We must keep in mind that a function where default parameters are used
must be assigned from right to left or for all the parameters. That’s why definition for fun1() doesn’t
point to any SyntaxError.
This discussion will be incomplete if we don’t use any mutable type as default parameter. Things can
get weird if you specify a default parameter value that is a mutable object, specifically an empty list.
Consider this Python function definition:
1 def fun(my_list = []):
2 my_list.append('e')
3 print(id(my_list), my_list)
4
5 fun()
6 fun(['a', 'b', 'c'])
7 fun()
First call fun(), takes a single list parameter, appends the string 𝑒 to the end of the list, and prints the
192
Python Fundamentals: A Functional Approach
If we didn’t pass any argument the default parameter is assumed, and an empty list is assigned to the
parameter my_list. But between the consecutive function calls if we didn’t pass any argument then the
same list is referred. So, after the 3rd call, we are getting ['e','e'] as output, and the id of my_list
is the same as first function call. While in the second call we have pass a list ['a', 'b', 'c'] as
argument and in this case default value is overwritten and new id is referred.
In Python, default parameter values are defined only once when the function is defined (i.e., when
the def statements are executed). The default value isn’t re-defined each time the function is called.
Thus, each time you call fun() without a parameter, you’re performing .append() on the same list.
To stop these duplicate items due to the default list we can assign a default value None to the
parameter. Later inside the function, we can check if the value of the parameter is None or not, and
based on that we can create the list and then append the value. See the code solution given below:
1471994294208 ['e']
1471994294208 ['a', 'b', 'c', 'e']
1471994294208 ['e']
In this way, we can ensure that an empty list will be created always if we didn’t pass any argument
while calling the function fun().
193
Python Fundamentals: A Functional Approach
The function void fun(int a) (at line 2) is defined and get called inside another function main().
Like python, in C programming also, we need to call the function to execute the code written inside the
function. In C programming, program execution started from the main() function (means every
program written in C must have one main() function).
In the above sample code at first a variable ‘a’ of type int is declared and initialized to 20 (at line
6). Thereafter ‘a’ is passed as argument to the function fun(a) (at line 7). While calling the function
the value of ‘a’ is passed and assigned to the parameter ‘a’ (listed in the function header at line 2) (a
copy of argument ‘a’). Later inside the function, the a’s value is changed to 40 (at line 3) and program
control is returned to the main() function.
In call by value, all the changes have been made to the copy of ‘a’ which is created inside the fun().
So, the changes aren’t reflected outside of the function fun() and the value of ‘a’ remains unchanged.
So, the printf("%d", a) statement prints the value of ‘a’ as 20.
In call by reference, reference represents the address of a memory block. So instead of passing the
value of the argument, we pass the address of the argument and changes made inside the function will
be done over the actual argument and reflected outside of the function. In Fig. 8.5 the flow of call by
value (in Fig. 8.5(a)) and call by reference (in Fig. 8.5(b)) is depicted.
While passing the argument in the call by reference we have passed the address of a fun(&a) and the
changes made by the statement in the header these two arguments passing approaches and later we have
re-written the above program and passed the argument by reference.
194
Python Fundamentals: A Functional Approach
See the above code at line 7 where the function fun(int *a) gets called and instead of the value of
‘a’, address of (&a) is passed. To hold the address of an argument, a pointer parameter type int *a is
used in the function header at line 2. To access the memory block of ‘a’, instead of a = 40, *a = 40
is used and the value ‘a’ is changed to 40.
The first statement declares a variable ‘a’ and refers to a memory block and later value 20 is kept
on it. The next statement overwrites 20 and puts 40 on it. The scenario in python is quite different.
Consider the statements given below:
>>>a = 20
>>>a = 40
The first statement causes ‘a’ to point to an object whose value is 20. The next statement reassigns
‘a’ to a new object whose value is 40. Stated another way, the second assignment rebinds ‘a’ to a
different object with value 40. In Python, when you pass an argument to a function, a similar rebinding
occurs. Consider this example:
1 def fun(x):
2 x = 10
3
4 a = 5
5 fun(a)
6 print(a)
In this program, the statement a = 5 (in line 4) creates a reference named ‘a’ bound to an object
whose value is 5. fun() is then called on line 5, with ‘a’ as an argument. When the function f()
executes first, a new reference called ‘x’ is created, which initially points to the same 5 object as ‘a’
does. However, when the statement x = 10 at line 2 is executed, function fun() rebinds ‘x’ to a new
195
Python Fundamentals: A Functional Approach
object whose value is 10. The two references, ‘a’ and ‘x’, are uncoupled from one another. Nothing
else that fun() does will affect ‘a’, and when fun() terminates, ‘a’ will still point to the object 5, as
it did prior to the function call. We can verify this using id() inside and outside of function fun(). See
the code given below:
1 def fun(x):
2 print(f'id(x) = {id(x)}')
3 x = 10
4 print(f'id(x) = {id(x)}')
5
6 a = 5
7 print('Before function call...')
8 print(f'id(a) = {id(a)}')
9 fun(a)
10 print('After function call...')
11 print(f'id(a) = {id(a)}')
Before function call...
id(a) = 2776095981936
id(x) = 2776095981936
id(x) = 2776095982096
After function call...
id(a) = 2776095981936
The above code is a slightly modified version of the previous function and along with the value of
‘a’, we are also checked the id. At first, ‘a’ is bind with the object 5 in line 6, and the id(a) before
and after the function call fun(a) didn’t change, even though we have assigned a new object to x inside
the function fun() at line 3.
There is another point we need to check that, the 𝑖𝑑(𝑥) at line 2 and 4 gets a different value. When
we check 𝑖𝑑(𝑥) at line 2, it is found that 𝑥 inside 𝑓() and 𝑎 outside of 𝑓(), represent the same 𝑖𝑑. But
once we have changed the object pointed by 𝑥 at line 3, 𝑥’s 𝑖𝑑 get modified.
We need to remember that, even though python passed arguments to a function as reference (not
like C programming), once we have assigned a new object that changes will not reflect outside of the
function. Argument passing in Python is somewhat of a hybrid between pass-by-value and pass-by-
reference. What gets passed to the function is a reference to an object, but the reference is passed by
value.
Note: Python’s argument-passing mechanism has been called pass-by-assignment. This is because
parameter names are bound to objects on function entries in Python, and assignment is also the process
of binding a name to an object. You may also see the terms pass-by-object, pass-by-object-reference, or
pass-by-sharing. The key takeaway here is that a Python function can’t change the value of an argument
by reassigning the corresponding parameter to something else. The following example demonstrates
this:
1 def fun(item):
2 item = 34
3
4 for i in (2,'Hello',[2,3,4],{'a':2,'b':3},{3,4},(3,5,6)):
5 fun(i)
6 print(i)
2
Hello
[2, 3, 4]
196
Python Fundamentals: A Functional Approach
{'a': 2, 'b': 3}
{3, 4}
(3, 5, 6)
Here we have passed objects of type int, str, list, dict, set and tuple to fun() as an
argument. The function fun() tries to assign an int object 34 and once the program control comes
back to the calling environment the value of the argument 𝑖 remains unchanged. Does that mean a
Python function can never modify its arguments at all? No, that isn’t the case! Watch what happens
here:
1 def fun(item):
2 if type(item) is list:
3 item.append(30) #appending an item to the list
4 elif type(item) is dict:
5 item.update({'c':9}) #updating the dictionary
6 elif type(item) is set:
7 item.add(40) #adding an item to the set
8
9 for i in ([2,3,4],{'a':2,'b':3},{3,4}):
10 fun(i)
11 print(i)
[2, 3, 4, 30]
{'a': 2, 'b': 3, 'c': 9}
{40, 3, 4}
Here we have passed mutable type objects as arguments to the function fun() and instead of
assigning a new object to the parameter item, we have updated the item for the type list, dict and
set. As shown in the output, we have found that once the program control returns to the calling
environment changes are made inside the function that reflect outside of the function for mutable types.
If the argument is a number or a string, the argument is not affected, regardless of the changes made to
the parameter inside the function.
1 def fun():
2 print('start of fun()')
3 print('end of fun()')
197
Python Fundamentals: A Functional Approach
4 return
5
6 fun()
7 print('after the fun')
start of fun()
end of fun()
after the fun
In this example there is no need for a return statement, which returns the program control back to
the calling environment. Even if we didn’t use the return statement, we are also getting the same output.
It is not necessary that return statement should be last statement of the function, even we can place it
anywhere inside the function, and even multiple times. Consider the sample code given below:
1 def fun(num):
2 if num > 0:
3 return
4 elif num < 0:
5 return
6 print('End of function')
7
8 fun(20)
9 fun(-1)
10 fun(0)
End of function
For the first two function calls at line 8 and 9, print statement at line 6 remain unreachable due to
the pre-execution of the return statement at line 3, and 5. The third call fun(0) doesn’t satisfy any of
the condition of the function body written at line 2 and 4 and due to this none of the return statement
is executed, so the print statement gets executed. Due to the misplaced of return statement sometimes
it is also possible that code written inside the function body never executed. Consider the sample code
given below:
1 def fun(num):
2 if num % 2 == 0:
3 return
4 elif num == 0:
5 return
6 else:
7 return
8 print('End of function')
9
10 fun(20)
11 fun(0)
12 fun(11)
The first two function call at line 10, 11 passed an argument to the function and subsequently the
conditions written at line 2 and 4 become True. Due to this return statements written at line 3 and 5
get executed. Like this, third call fun(11) doesn’t satisfy any of the conditions and the else clause
having a return statement gets executed. In this example, multiple return statements are used and get
executed before reaching the last statement at line 8 for all the function calls and the print statement is
never executed.
Note: Once the function reached any of the return statement, lines written after it were never
198
Python Fundamentals: A Functional Approach
considered for the execution. This sort of paradigm can be useful for error checking in a function. You
can check several error conditions at the start of the function, with return statements that bail out if
there’s a problem.
def fun(optional_arguments):
if error_cond1:
return
elif error_cond2:
return
elif error_cond3:
return
#Actual body of the function goes here
If none of the error conditions are encountered, then the function can proceed with its normal
processing.
199
Python Fundamentals: A Functional Approach
<class 'tuple'>
<class 'int'> <class 'int'>
<class 'list'>
The swap() function called thrice at line 6, 8, and 10, where function returns the values of a and b
as integer after swapping. The first call at line 6, receives the return values in one variable, as tuple and
this mechanism is called packing. The second call at line 8 uses two variables to hold the return values,
where the number of variables is same as number of return values returns from the function. The third
call received the return values as a list, as swap() get called inside list() function. In Python, it is also
possible for a return statement did return nothing, instead returns the special Python value None, like
the pass statement, as shown below:
def fun1():
pass
def fun2():
return
def fun3():
print()
200
Python Fundamentals: A Functional Approach
▪ Define an outer function that creates an inner function. The inner function can access variables
from the outer function's scope, even after the outer function has finished executing. This is
because the inner function remembers the state of the outer function at the time it was created.
▪ The outer function returns the inner function.
Let’s take one scenario where you need to create functions whose behavior depends on external factors
or user input. Returning a function allows you to capture this dynamic behavior and create functions
tailored to specific situations, such as unit converter.
1 def get_converter(unit_from):
2 """
3 This function takes a unit type and returns a function that converts
4 temperatures from that unit to Celsius.
5 """
6 def convert_to_celsius(value):
7 """
8 This function converts a temperature value to Celsius based on the unit_from.
9 """
10 if unit_from == "fahrenheit":
11 return (value - 32) * 5 / 9
12 elif unit_from == "celsius":
13 return value
14 else:
15 raise ValueError("Unsupported unit type:", unit_from)
16 return convert_to_celsius
17
18 # Get a converter function for Fahrenheit to Celsius
19 celsius_converter = get_converter("fahrenheit")
20
21 # Use the converter function
22 converted_value = celsius_converter(77)
23 print(f"{converted_value:.2f} degrees Celsius")
▪ Line 1: Defines a function named get_converter that takes a single argument unit_from.
▪ Line 6: Defines another function named convert_to_celsius inside the get_converter
function. This is the nested function.
▪ Lines 10-11: If unit_from is "fahrenheit", it converts from Fahrenheit to Celsius using the formula.
▪ Lines 12-13: If unit_from is "celsius", no conversion is needed, so the value is returned directly.
▪ Lines 14-15: If unit_from is not supported, it raises a ValueError exception.
▪ Line 16: The return statement in the inner function (convert_to_celsius) returns the calculated
value or the original value depending on the unit. This return statement within the inner function
is crucial. It's what makes get_converter a function that returns a function.
▪ Line 18: Calls the get_converter function with the argument "fahrenheit". This execution creates
a new instance of the inner function (convert_to_celsius) specifically for converting from
Fahrenheit.
▪ Line 19: Assigns the returned function (the conversion function for Fahrenheit to Celsius) to the
variable celsius_converter.
201
Python Fundamentals: A Functional Approach
▪ Line 21: Calls the celsius_converter function (which is the inner convert_to_celsius
function) with the value 77 (presumably Fahrenheit).
In summary, the get_converter function acts as a factory that creates conversion functions based on
the provided unit type. This allows for code reusability and avoids writing separate conversion functions
for each unit.
0.2
0.6
1.2
2.0
3.0
But this approach still suffers from a couple of problems. For starters, it still only allows up to five
arguments, not an arbitrary number. Worse yet, there’s no way to distinguish between the arguments
that were specified and those that were allowed to default. The function has no way to know how many
arguments were passed, so it doesn’t know what to divide by and always it gets divided by 5.
Without knowing the number of parameters passed from the calling environment it is difficult to
return the correct average value by the function. To know the exact number of arguments, a calling
environment can pass a list of items as argument and function can use len() to know the number of
items, to produce the exact average value.
1 def avg(nums):
2 total = 0
3 for item in nums:
4 total += item
5 return total / len(nums)
6
7 print(avg([1]))
202
Python Fundamentals: A Functional Approach
8 print(avg([1,2]))
9 print(avg([1,2,3]))
1.0
1.5
2.0
At least this works. It allows an arbitrary number of values and produces a correct result. As a bonus,
it works when the argument is a tuple as well. The drawback of this approach is that the user must be
aware that to group the values into a list or tuple is probably not something the user of the function
would expect, and it isn’t very elegant. To make the code more user-friendly python provides a better
approach to pass variable number of arguments to a function using following methods:
▪ Argument packing as tuple using an asterisk (*) with the parameter name
▪ Argument packing as key = value pair as dictionary using double asterisk (**) with parameter
name
In the above example function avg() is rewritten using the tuple packing mechanism by preceding
the parameter args by an asterisk (*) in the function header. While receiving, parameters are packed as
a single object of type tuple and inside the function the args considered as tuple and while calling the
function, the user no need to do anything extra to group the arguments either as list or tuple as we did
for the last example.
a: 2
203
Python Fundamentals: A Functional Approach
a: 2 b: 3
a: 2 b: 3 c: 5
The double asterisk (**) is used with function parameter name to specify the dictionary packing. In
the above code fun() get called three times with variable arguments as key = value pair. Preceding a
parameter name in a Python function definition by double asterisk (**) indicates that the corresponding
arguments, which are expected to be key = value pairs, should be packed into a dictionary. While
packing the arguments to a dictionary the name kwargs is used, you can use any other name. Here
kwargs represents keyword argument and from the output of the code it is clear about the type of
parameter kwargs as dictionary.
▪ unpacking an iterable object such as tuple, list and set using an asterisk (*) with the argument
name
▪ unpacking a dictionary as keyword argument using double asterisk (**) with argument name
An asterisk (*) is placed before the name of the argument to unpack an iterable object before passing
it as argument. The function call at line 5 and 7 uses an asterisk (*) before the name of argument args
and items of the object are unpacked and then pass it to the function avg() as positional arguments.
While unpacking any iterable it must match the number of positional parameters in the function header,
else it will raise a TypeError.
We can also combine the packing and unpacking mechanism while passing the arguments to a
function. Let’s write a more user-interactive code to take input from user to find the largest of all the
items. Consider the example given below:
1 def find_largest(*args):
2 max = args[0]
3 for i in range(1, len(args)):
4 if args[i] > max:
5 max = args[i]
6 return max
7
8 args = eval(input('Enter comma-separated values:'))
204
Python Fundamentals: A Functional Approach
As of now whatever the strings we are sending it is always concatenates with a period (.) only. What
if you want to modify the function to accept an argument which accepts the character also which will
be used for this purpose, as the solution given below:
>>> def joinstr(char, *args):
... print(char.join(args))
...
>>> joinstr('#','a','b','c')
a#b#c
This function could solve our purpose, but there are a couple of undesirable things about this
solution:
▪ How could someone know that the first argument serving a specific purpose while joining
other variable number of strings?
▪ The parameter char is not optional, it must be passed always.
One of the ways we can satisfy the above requirements, by making the argument char as default
argument, which makes it optional. Let’s us try this:
>>> def joinstr(char='.', *args):
... print(char.join(args))
...
>>> joinstr('a','b','c')
bac
205
Python Fundamentals: A Functional Approach
Unfortunately, the solution does not work, because the first string argument 'a' is considered as the
special character argument, which is used for joining. Now let’s us try by specifying a keyword argument
before the variable number of strings as follows:
>>> joinstr(char='#','a','b','c')
File "<stdin>", line 1
joinstr(char='#','a','b','c')
^
SyntaxError: positional argument follows keyword argument
This also does not works, and the solution leads to error as positional argument follows keyword
argument. So, neither of the solutions solve our purpose. Let’s see how Python can help us. Syntactically
the solution is very simple. The first change is to allow the optional argument to appear after a *args
argument in function definition.
>>> def joinstr(*args, char='.'):
... print(char.join(args))
...
>>> joinstr('a','b','c',char='#')
a#b#c
Keyword-only arguments allow a Python function to take a variable number of arguments, followed
by one or more additional options as keyword arguments. If you wanted to modify joinstr() so that
a separator (sep) along with an ends character can optionally be specified as well, by adding an additional
keyword-only argument:
>>> def joinstr(*args, sep = ' ', ends = '.'):
... print(sep.join(args) + ends)
...
>>> joinstr('This', 'is', 'Python', '3.10', sep = '|', ends = '!')
This|is|Python|3.10!
The following points need to remember to work with keyword-only arguments: (a) If the keyword
argument didn’t pass at the time of calling then the default value will be considered. (b) If the default
value didn’t provide then passing the values for the keyword argument is compulsory. Not doing so will
raise an error.
The second approach to implement the keyword-only argument is achieved by a syntactical change,
which allow the argument name to be omitted for a variable list argument (varargs), as follows:
>>> def fun(a, b, *, karg = None):
... pass
This approach is useful when we want to define a function is not allowed to take variable list arguments
and take a keyword argument. Consider the example given below, which performs a designated
operation over two arguments:
1 def calculator(a, b, ope='+'):
2 if ope == '+':
3 return a + b
4 elif ope == '-':
5 return a - b
6 elif ope == '*':
7 return a * b
206
Python Fundamentals: A Functional Approach
For the above function the type of operation performed it depends on the ope parameter. If we want
the ope argument should pass as keyword only, we can add an unnamed ∗ parameter before the ope. In
this case the ope argument become compulsory to pass as keyword-only or optional, because of the
default value assigned to it. Now check the next three function calls:
>>> def calculator(a, b, *, ope='+'):
... if ope == '+':
... return a + b
... elif ope == '-':
... return a - b
... elif ope == '*':
... return a * b
... elif ope == '/':
... return a - b
... else:
... return None
...
>>> calculator(3,4, ope = '-')
-1
>>> calculator(3,4)
7
>>> calculator(3,4,'-')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: calculator() takes 2 positional arguments but 3 were given
First two calls work fine, but the third call pass the ope argument as positional which leads to an
exception. Because it considered it as positional argument. What if you pass some extraneous positional
argument with two required positional arguments and keyword-only argument, as follows:
>>> calculator(3,4,"no extra values",'-')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: calculator() takes 2 positional arguments but 4 were given
The calculate() function is not ready yet to take any extra positional argument that can be ignored by
the function. For this we can make a dummy argument by using a name with the ∗ and later ignored by
the function this as follows:
>>> def calculator(a, b, *ignore, ope='+'):
... print(ignore)
... if ope == '+':
... return a + b
... elif ope == '-':
... return a - b
... elif ope == '*':
... return a * b
... elif ope == '/':
... return a - b
... else:
207
Python Fundamentals: A Functional Approach
Above two function calls don’t test the actual meaning of fun(), which accepts a and b as positional
only. Let’s try to send any of these two parameters as keyword and trying to do so leads to a TypeError
as fun() expecting a and b as positional but pass as keyword.
>>> fun(a = 2, b = 3, c = 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fun() got some positional-only arguments passed as keyword arguments: 'a,
b'
The positional-only arguments can be mixed with keyword-only arguments also, if it is necessary as
follows:
>>> def fun(a, b, /, *, c):
... print(a, b, c)
...
>>> fun(2, 3, c = 4) #c must be pass keyword only
2 3 4
In the above function definition, an unnamed argument ∗ is used after the forward slash (/) and it
makes anything passed after the positional argument b should be passed as keyword only and not doing
so will raised a TypeError.
tool. The following two string literal types can’t be retrieved using the runtime object __doc__.
▪ String literals occurring immediately after a simple assignment at the top level of a module, class,
or __init__ method is called “attribute docstrings”.
▪ String literals occurring immediately after another docstring are called “additional docstrings”.
This section highlights docstrings related to a function and docstring related to class and module is
discussed in vol. 2 of this book. In python following ways we can prepare the docstrings:
▪ For consistency, always use """triple double quotes""" around docstrings.
▪ To represent a backslash as part of the docstring use r"""raw triple double quotes""".
▪ For Unicode docstrings, use u"""Unicode triple-quoted strings""".
Note: A docstring is used to supply documentation for a function. It can contain the function’s purpose,
what arguments it takes, information about return values, or any other information you think would be
useful.
The runtime object __doc__ is used to retrieve the docstring of a function, as shown in the output.
The one-line docstring should NOT be a “signature” of a function reiterating the function/method
parameters (which can be obtained by introspection). Don’t do:
209
Python Fundamentals: A Functional Approach
>>> help(fun1)
Help on function fun1 in module __main__:
fun1(a, b)
fun1(a,b) -> list
Keyword arguments:
real: the real part (default 0.0)
imag: the imaginary part (default 0.0)
Returns:
A complex number.
"""
Like single line docstring multi-liner is also retrieve by either help() function or special attribute
__doc__ as shown above. Further details about the docstring indentation, docstring inside a class or
module read the documents provided as footnote23,24.
23
D. G. a. G. v. Rossum, “PEP 257 – Docstring Conventions,” 29 5 2001. [Online]. Available:
https://peps.python.org/pep-0257/.
24
D. Goodger, “PEP 258 – Docutils Design Specification,” 31 5 2001. [Online]. Available:
https://peps.python.org/pep-0258/#attribute-docstrings . [Accessed 22 10 2022]
210
Python Fundamentals: A Functional Approach
****************END OF CHAPTER****************
211
ABOUT THE AUTHOR
Dr. Goutam Majumder is an esteemed academic and researcher in the field of Computer Science &
Engineering. Born on August 29, 1986, in India, Dr. Majumder has made significant contributions to
his field over a career spanning more than 13 years. He completed his Doctorate in Computer Science
& Engineering from the National Institute of Technology Mizoram in 2021. Prior to his Ph.D., he
earned his M.Tech from Tripura University (A Central University), ranking first in the Science &
Technology Department, and his B.E. from the National Institute of Technology Agartala. He also
completed PG Diploma in Advance Computing from C-DAC Acts, Pune. Dr. Majumder has held
prestigious positions as an Assistant Professor at Vellore Institute of Technology (VIT) Vellore since
January 2023 and previously at Lovely Professional University and NIT Mizoram. His research has been
recognized with a Scopus ID and WoS ID, and he maintains an active presence on academic platforms
like Google Scholar and ResearchGate. In addition to his academic pursuits, Dr. Majumder is a member
of the Computer Society of India and has completed certifications in Java, Python, and Machine
Learning from renowned institutions such as Oracle, Hackerrank, Coursera, and NPTEL. Dr.
Majumder’s dedication to education and research is evident in his extensive experience and certifications,
marking him as a distinguished figure in the Computer Science community. He can be contacted at
[email protected]
Dr. Ganesh Khekare is currently working in the Department of Computer Science & Engineering at
Vellore Institute of Technology, Vellore, India (Deemed University NIRF Rank 08). He has done BE,
ME, and PhD in Computer Science and Engineering and currently pursuing Post Doc from the
University of Lincoln, Malaysia. He is UGC net exam qualified with 98.15 percentiles. He has been a
member of different Societies and professional bodies like ACM since 2014, a lifetime member of the
ISTE, IEI, ISDS, and IEEE Senior Member, IAENG, IFERP, and IERD member since 2018. He has
more than 15 years of teaching and research experience. His main research work focuses on Data
Science, the Internet of Everything, Machine Learning, Networking, Artificial Intelligence, etc. He has
published more than 100 research articles in reputed international journals and conferences including
Thomson Reuters, ACM, IGI Global, Inderscience, Springer, IEEE, Taylor & Francis, etc. He has more
than 1000 Google Scholar citations. He has done 10 patents and 20 copyrights including international
Canadian copyright. Guided more than 50 UG as well as PG research projects, currently guiding 6 PhD
research scholars. He has organized numerous international conferences, seminars, FDPs, and
workshops. He has delivered several invited talks. He is an editorial board member, international
advisory board member, associate editor, and guest editor for many International Journals. He serves as
a reviewer for various reputed International Journals and Conferences. He participated in many
international conferences worldwide as a core organizing committee member, technical program
committee member, and Session Chair. Served as a resource person for more than 100 national and
international events. Certified with more than 40 training programs from various NITs, IITs, and
reputed industries like TCS, Persistent, etc. on the latest technologies. He can be contacted at
[email protected]
212