

Strings, lists, tuples, integers, floats, and dictionary are some of the types that we have already encountered.
In this chapter, we will organize and classify them into fewer categories to make sense of the Python type system. This classification of Python built-in types is called Standard Type Hierarchy.
We can classify various built-in Python types under different categories. We can see one such classification in the figure 1.
We can classify the built-in types under,
- Singletons
- Numbers
- Collections
- Callables
We will look at each one of them, starting with Singletons.
Singletons
The Singletons are built-in types that have a single value, and there exists only a single object having the type. The list of built-in singleton types is the list in the table 1.
Type | Name | Truth Value |
---|---|---|
NoneType | None | False |
NotImplementedType | NotImplemented | True |
elipsis | Elipsis or ... |
True |
Let's look at the first singleton: NoneType.
None
There exists only one object with the type NoneType in Python, and we can access it through the built-in name None
. We use the None
object to signify the absence of value in many cases.
Functions that do not explicitly return anything return the None
object by default. Let's check it out using a code sample.
As you might recall, the print()
function doesn't return any value. Therefore, if we assign its function call to a name, we will get a NoneType
object.
>>> greet = print("Hello")
Hello
>>> type(greet)
<class 'NoneType'>
As there is only one instance of None
object through Python, we can use the identity operator is
to verify.
>>> greet is None
True
If we try to assign a different object to the name None
, Python raises SyntaxError
. This is because None
is one of Python's keyword.
>>> None = "Something else"
File "<stdin>", line 1
SyntaxError: can't assign to keyword
A NoneType
is one of few built-in objects whose truth value is False
.
>>> bool(None)
False
What's the output of the code below?
>>> if not None:
print("Hello World")
Hello World
- raises
NameError
- raises
ValueError
- The
if
block doesn't execute
As the truth value of the None
is False
, not None
always executes as True
. Hence the code in the previous exercise always executes. The next singleton type is the NotImplementedType
object.
NotImplemented
There exists only one object with the type NotImplementedType
in Python. We can access it through the built-in name NotImplemented
.
>>> type(NotImplemented)
<class 'NotImplementedType'>
Unlike None
, you can reassign the name NotImplemented
to something else.
>>> NotImplemented = "Something else"
>>> NotImplemented
'Something else'
Although Python wouldn't complain about this re-assignment, you should never reassign or overwrite the name NotImplemented
.
The truth value of a NotImplementedType
object is True
.
>>> bool(NotImplemented)
True
We can return the NotImplementd
object for special binary methods such as __eq__()
, __lt__()
or __add__()
to iindicate the operation is not implemented with respect to other type. We will cover this in more detail when we cover creating custom objects in subsequent courses.
The next singleton type is Ellipsis
.
Ellipsis
Ellipsis
type object has a single value, and there is only a single object in Python with this value. We can access this object through the literal ...
or the built-in name Ellipsis. The truth value of an Elipsis
type object is True
.
One of the usages of the Ellipsis is to indicate incomplete function definition.
>>> def foo():
...
>>> foo()
We can use any string literal instead of ...
Ellipsis.
For example, in the below code listing, we use a string object.
>>> def foo():
"Incomplete Code"
>>> foo()
However, many developers prefer using the Ellipsis object to indicate an incomplete function code as per a not-so-strict convention. Similarly, sometimes programmers use the pass
keyword to indicate an empty code block or future implementation.
>>> def foo():
pass # Empty function
In Python, the pass keyword is a null statement. Unlike comments, the python interpreter does not ignore a pass statement. However, nothing happens when Python executes the pass statement.
Python built-in types do not generally use the Ellipse object.
Like the None object, the ...
object is also a keyword and cannot assign a new value. The truth value of Ellipsis is True
.
>>> ... = "Something Else"
File "<stdin>", line 1
SyntaxError: can't assign to Ellipsis
That brings us to the end of Singleton types. Next, we will look into the standard types in Python, which are used to represent numeric types.

Numbers
Integer literals, Floating-point literals and Imaginary number literals to create numeric type objects.
Python uses four built-in numeric types: Booleans, integers, complex numbers, and floating-point numbers for representing numbers. Additional two numeric types: Decimals and Fractions are available upon import.
We can group Booleans and Integers as Integral, while we can group other numeric types under Non-Integral.
Integral numeric types represent elements from the mathematical set of integers (positive and negative). Let's start with Integers.
Integer
These represent numbers in an unlimited range, subject to available (virtual) memory only. You can create an object with an integer type using integer literal or using the built-in int() function.
>>> a = 13 # Creating an integer object using integer literal
>>> b = int(12) # Creating an integer using `int()` function
The int()
function can convert a string literal containing integer to an integer object. For example,
>>> int("666")
666 # Integer
int()
function is also called a constructor. Can you guess why it is called so?
In Python, constructors are used to construct an object of specific class or type. The built-in int()
function is a constructor, which is used to create integer objects.
You can also convert integers from different number bases to integers in base-10. The built-in int()
function has the following function signature.
$$
int(x=0, base =10) \rightarrow \text{integer object} \tag{1}
$$
We can convert a number from a base other than $10$ by enclosing in a string or adding an appropriate prefix depicting the base.
>>> int() # without arguments
0
>>> int("1010", 2) # Convert the 1010 from base 2
10
>>> int("100", 8) # Convert 100 from base-8
64
>>> int("10E", 16) # Convert 10E from base-16
270
We can also directly convert a number is represented in other base, which has been prefixed to represent its base. To recall, table 2 shows some of the prefixes used to depict the base that the Python interpreter understands.
Base | Number | Prefix | Example |
---|---|---|---|
2 | Binary | 0b |
0b100 |
8 | Octal | 0o |
0o100 |
10 | Decimal | No Prefix |
100 |
16 | Hexadecimal | 0x |
0x10E |
You can create an integer literal directly using the base prefix, and Python will automatically convert to its _base-10_ counterpart. In the built-in function `int()`, if you use an integer literal with a base prefix, you don't have to specify the base.
>>> 0b1010 # Integer with Binary Prefix
10
>>> 0o100 # Integer with Octal Prefix
64
>>> 0x10E # Integer with Hexadecimal Prefix
270
>>> int(0b100) # Integer with Binary Prefix
10
>>> int(0o100) # Integer with Octal Prefix
64
>>> int(0x10E) # Integer with Hexadecimal Prefix
270
To convert numbers from base-10 to different bases, you can use the built-in functions bin()
, oct()
, and hex()
for binary, octal decimal, and hexadecimal, respectively.
For instance, to convert a decimal number fourteen to binary, you can pass the 14
integer to the bin()
function, and Python will return the corresponding binary representation.
hex(270)
on Python?
In the previous code listing, we converted numbers from different bases to decimal base. We can see that the 0x10E
is 270 in the decimal. Therefore, hex(270)
will be equal to be 0x10E
.
We can verify this using the interpreter.
>>> bin(10)
'0b1010' # String object representing base-2
>>> oct(64)
'0o100' # String object representing base-8
>>> hex(270)
'0x10e' # String object representing base-16
In computing, bit-length is the number of bits required to represent an integer object in binary. You can use the built-in method bit_length()
of an integer.
>>> bin(24)
'0b11000' # 5 bits required to represent the number
>>>(24).bit_length() # Integer literal must be wrapped with parens
5
>>> a = 24
>>> a.bit_length()
5
What do you think will be the bit length of the number 29?
- 9
- 10
- 8
- 7
Integer Caching
Earlier, we saw that when we create an object and assign it a name, the reference count of the object increases. However, something interesting happens when we assign names to integers. Let's take a look at the code sample below.
Let's create two objects with the value 257
and name them a
and b
.
>>> a = 257 # Assign `a` to an integer object with value 257
>>> b = 257 # Assign `b` to an integer object with value 257
Now, let's if check both names refer to the same object.
>>> a is b # Check if both of them refer to the same object.
False
Therefore, Python creates different objects with the same value, 257
.
Now, let's repeat the experiment with 256
.
We will define two names, c
and d
, and assign them to 256
.
>>> c = 256 # Assign `c` to an integer object with value 256
>>> d = 256 # Assign `d` to an integer object with value 256
Now, let's check if both the names (c
and d
) refer to the same object.
>>> c is d
True # Hmmm. Both are pointing to the same object.
So, what's going on here?
Python assigns names to the same object with the value 256
while assigning names to different objects with integer value 257
. This is not a bug.
Python caches small integers, which are integers between -5 and 256. Python stores the integers between -5
and 256
, which are likely to be used repeatedly in a program. In Python, the cache of integers between -5 and 256 is called small integer cache. The official Python docs refer to this as follows:
The current implementation keeps an array of integer objects for all integers between -5 and 256. When you create an int in that range, you get back a reference to the existing object.
Note: Depending on your environment and Python compiler, you might get different results. Small integer cache is for understanding only, and do not try to use this in a program as the number of caches might change.
Caching
Caching is a process where we store information that is likely to be frequently accessed for easier access.
When you visit a website, your browser takes pieces of the page and stores them on your computer's hard drive.
The browser stores assets such as Images (logos, pictures, backgrounds, etc.), HTML, CSS
and Javascript
file.
In short, it typically caches what are known as static assets - parts of a website that do not change from visit to visit.
Caching speeds up browsing a website. Once you've downloaded an asset, it is kept on your hard-drive storage unless you clear the cache.
The next time you visit the website, part of the website is loaded from your hard-drive than your remote server. Retrieving files from your storage will always be faster than retrieving them from a remote server.
Python does a similar thing with the small-integer cache. It already instantiates some of the integers that are most likely to be used. The next integral type is Boolean
.
Boolean
The Boolean type has two possible values: True
and False
. Booleans are classified under Integral types.
The implication is that Boolean
inherits the characteristics of integers and essentially are integers. The type bool
is a subclass of type int
, i.e., Boolean extend from the integer int
type.
We can verify this using the built-in function isinstance()
.
The built-in isinstance()
function checks if a given object is an instance of a class
or a subclass
.
>>> type(False)
<class 'bool'>
>>> isinstance(False, int) # Check if False is an instance of type `int`
True
>>> isinstance(False, bool) # Check if `False` is an instance of type `bool`
True
The integer value of True
is 1, while for False
, it is 0.
>>> int(True)
1
>>> int(False)
0
As Boolean extend from type int
, they behave effectively as integers.
>>> True + 5
6
>>> False - 5
-5
You can use them as an index key to access an item in a container, and Python won't complain.
>>> a = [1,2,3,4]
>>> a[True] # same as a[1]
2
>>> a[False] # same as a[0]
1
However, it would be best not to use True
and False
as integers as it will confuse other developers.
Boolean can be constructed by:
- Boolean expressions using comparison operators such as
5
>6
- using built-in
bool()
function which we earlier used to test truth values such asbool([1,2])
The Booleans objects are constants and part of Python's reserved keyword, which means you cannot reassign their value to something else.
>>> True = "Not True"
File "<stdin>", line 1
SyntaxError: can't assign to keyword
In the next section, we will go into more details about non-integral types.
Non-integral Types
Floating-point numbers, decimals, complex numbers and fractions are classified under non-integral. We will start with floating-point numbers.
Floating Types
As you might recall, a floating-point value or float is a numerical literal that contains a decimal point. The floating-point type can store fractional numbers.
They can be defined either in standard decimal notation or in exponential notation.
>>> 1.50 # Standard Decimal Notation
1.5
>>> 15e-1 # Exponential Notation
1.5
We can create Floating-point number objects from floating-point literals or the float()
constructor.
>>> 3.4e3 # Float literal
3400.0
>>> float(3.4e3) # `float` constructor
3400.0
We can convert an integer to float using the float()
constructor.
>>> a = 2
>>> a # integer
2
>>> float(a)
2.0 # float
Similarly, we can convert a float into an integer with an int()
constructor. The int()
constructor removes the decimal part and returns the integer number.
>>> int(3.1451)
3
Upon division, numbers often produce floats with many decimal digits, which might not be what you want while printing.
>>> a = 45/7
>>> print("45 upon division by 7 yields ", a)
45 upon division by 7 yields 6.428571428571429
You can control the precision of a float by using the built-in round()
function.
>>> round(3.45871, 3) # Rounds off to 3 decimal places
3.459
>>> round(3.45871 , 2) # Rounds off to 2 decimal places
3.46
>>> round(3.45871 , 1) # Rounds off to 1 decimal places
3.5
How does Python evaluate the following expression?
>>> round(3.45871)
Error
4
3.5
3
You might recall that the built-in round()
function rounds off to the nearest integer when no precision is specified.
If you wish to get the nearest integer lower than the value or greater than the value, you can use the floor()
and ceil()
function from the math
module, respectively.
Let's see them in action.
We will first import the ceil()
and floor()
functions from the math
module.
>>> from math import ceil, floor
Now, let's get the nearest integers of 3.56.
# Get the nearest integer lower than the value
>>> floor(3.56)
3
# Get the nearest integer higher the the value
>>> ceil(3.56)
4
You can use basic arithmetic operations on the floats with other floats or even integers.
>>> 3 + 0.5 # Addition
3.5
>>> 3 - 0.5 # Subtraction
2.5
>>> 3 / 0.5 # Division
6.0
>>> 3 * 0.5 # Multiplication
1.5
And compare with other floats or integers.
>>> 3 > 0.5
True
>>> -0.5 > 0.5
True
However, you should take a bit of caution when using equality floats. For example,
>>> 0.1 + 0.2 == 0.3
False
0.1+0.2
should be equal to 0.3
. Why do you think Python gives the wrong result?
To put it simply, a floating-point number doesn't always exactly represent the number it seems to be representing. For instance, the actual value of float 0.1
is slightly more than 0.1
. To understand why we need to know how Python stores floating numbers.
Issues and Limitations of using Fractions in Python
Python implements the Floating-point numbers in the computer hardware as binary fractions. Earlier, we saw how can integers in base-10 be represented as base-2 numbers. Similarly, we can also represent fractions in the form of binary fractions.
In base-10, we can represent the decimal fraction of 0.125 as
$$
0.125 = 1\times10^{-1} + 2\times10^{-2} + 5\times10^{-3} \tag{2}
$$
Similarly, the binary fraction 0b0.001 can be expressed as following in decimal.
$$
\begin{equation}
\begin{split}
0\text{b}0.001 & = 0\times2^{-1} + 0\times2^{-2} + 1\times2^{-3} \\
& = \frac{0}{2} + \frac{0}{4} + \frac{1}{8} \\
& = 0 + 0 + 0.125 \\
& = 0.125
\end{split}
\end{equation}
\tag{3}
$$
Both 0.125 and 0b0.001 have the same values, although written in different notations. However, representing the fractions in binary poses a particular challenge.
We cannot precisely represent most decimal fractions as binary fractions.
To understand why, let's first look at the decimal value of the fraction 4/3
in the decimal system.
$$
\frac{4}{3} = 1.33333333... \tag{4}
$$
The value of the fraction results in an infinitely repeating decimal part.
The fraction $4/3$ results in an infinitely repeating decimal; therefore, we cannot represent $4/3$ exactly in decimal representation.
We can round off or approximate the value of the fraction $4/3$ to finite digits of the decimal part to use the value. To do this, we will use the built-in round()
function.
The listing below shows the three different approximations of the fraction $4/3$.
>>> round(4/3, 2)
1.33
>>> round(4/3,3)
1.333
>>> round(4/3, 4)
1.3333
No matter how many digits you round off, the result will never be equal to the fraction $4/3$; rather will be an increasingly better approximation of $4/3$. Thus, we cannot represent the fraction $4/3$ in the form of a decimal number with a finite number of digits.
Similarly, no matter how many base-2 digits you use, the decimal value 0.1
cannot be represented precisely as a binary fraction. In base-2, the $1/10$ fraction has an infinitely repeating fractional part.
$$
\frac{1}{10} = 0b0.000110011001100110011001100110011... \tag{5}
$$
Any higher number of bits will get you a closer approximation to the value of $0.1$ but never exact.
Representation error refers to the fact that most decimal fractions cannot be represented accurately as binary fractions.
Representation error is the chief reason why Python and other programming languages such as C++, Java often won't display the exact decimal number you expect.
The Institute of Electrical and Electronics Engineers, Inc. (IEEE) defined standards for floating-point representations and computational results to deal with the problem relating to the inexact conversion of decimal fractions into binary usually referred to as IEEE-754. Almost all machines use the IEEE-754 floating-point arithmetic, and almost all platforms map Python floats to IEEE-754 specified 53-bit precision.
This means that when you input 0.1
float in Python, the underlying machine strives to convert 0.1 to the closest fraction to the form $J/{2^N}$, where J is an integer containing exactly 53 bits.
$$
\text{decimal number} ~~~ \tilde= ~~~ \frac{J}{2^N} \tag{6}
$$
We can understand this better. Let's convert the decimal number 0.1 or $1/10$ to the closest figure that we can represent with 53-bit precision.
$$
\begin{align*}
\frac{J}{2^N} ~ ~ &\tilde= ~~\frac{1}{10}\\
J &= ~~\frac{2^N}{10}
\end{align*}
\tag{7}
$$
In equation 7, to find the value of J, we need to figure out N's value with the keeping in mind that it can represent the integer with exactly 53-bits.
We can use the built-in method integer method bit_length()
.
Earlier, we checked a built-in Python method, bit_length()
of integers, to check the numbers of bits required to represent a number. Let's check for different values of $N$ for which the value of $J$ will have exactly 53 bits.
>>> int(2**54/10).bit_length()
50
>>> int(2**55/10).bit_length()
52 # Less than 52
>>> int(2**56/10).bit_length()
53 # Equals 53
>>> int(2**57/10).bit_length()
54 # Exceeds 53
Thus $56$ is the only value for N that leaves J with exactly 53 bits.
Equation 7 can be written as
$$J=\frac{2^{56}}{10} \tag{8}$$
We can get the value of J in Python using the built-in divmod()
function. The divmod()
function takes number and divisor as arguments and returns a tuple containing the value quotient and remainder ( x//y
, x%y
).
>>> divmod(2**56, 10)
(7205759403792793, 6) # ( Quotient, Remainder )
The remainder $6$ is more than half of the divisor $10$, so to get the value of $J$, let's add $1$ to the quotient.
>>> J = 2**56 // 10 + 1
>>> J
7205759403792794
To get the approximate value of 0.1
, we will divide J by 256.
>>> 2**56
72057594037927936
>>> J / 2**56
0.1 # The approximate value of 0.1 that computer stores
Therefore the best possible approximation to 1/10 in IEEE-754 standard is
$$
\begin{equation}
\begin{split}
\frac{1}{10} ~~~ &\tilde= ~~~ \frac{7205759403792794}{72057594037927936}\
\
&= \frac{3602879701896397}{36028797018963968}
\end{split}
\end{equation} \tag{9}
$$
To see the value of the fraction out to 55 decimal digits, we can multiply the fraction by $10^{55}$.
>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625
The exact number stored in the computer for the float 0.1 is $0.1000000000000000055511151231257827021181583404541015625$.
0.1
, Python uses the above number instead of using exactly 0.1
. Do you think it affects calculations involving fractions?
Fractions are stored approximately to the exact number. As you can see, the numbers are accurate to a lot of significant digits. Therefore, for most practical purposes, it won't affect your calculations.
Instead of displaying the full decimal value, many programming languages, including Python, round off the number to 17 significant digits. To verify, we can check the digits till 17 digits using the built-in format()
function.
The format()
function takes another argument that specifies the precision value. You can read more about the format()
using the help()
function.
>>> format(0.1, '.17f')
'0.10000000000000001'
We can see that Python interprets 0.1
as something other than the fraction 1/10, which is a close approximation. For 0.3
also, Python stores an approximation rather than the exact value.
>>> format(0.3, '.17f')
'0.29999999999999999'
Whenever you input 0.1
into the interpreter, Python interprets to the closest approximation. Python rounds off the float to a single-digit giving an illusion that it's the same as the fraction when it's not.
To check which fraction is used to represent a decimal number, Python has a method as_integer_ratio()
for floats, which returns a pair of integers whose ratio is exactly or approximately equal to the original float and with a positive denominator.
>>> 0.1.as_integer_ratio()
(3602879701896397, 36028797018963968) # Same as we obtained
>>> 0.5.as_integer_ratio()
(1, 2) # 0.5 = 0b0.1
Now that we have covered how python stores and works with floats, it might be a bit clear why $0.1 + 0.1 + 0.1$ is not equal to $0.3$ in Python.
This limitation of Python impacts business applications and fields that require precise arithmetic calculations.
Fields such as accounting and finance mostly require positively require highly-precise and accurate calculation.
To overcome the limitation of the floating-point, Python has another numeric type, Decimal
, which can be used for exact addition. Decimal, for floating-point numbers with user-definable precision.
Let's look into them next.
Decimals
We saw earlier that numbers like $0.1$ don't have an exact representation in binary floating-point numbers. In contrast to the built-in floating-point type number, the numeric type Decimal
can represent numbers accurately. As the Decimal
type can represent numbers accurately, the corresponding arithmetic operations are accurate as well.Decimal
type objects can be created by first importing the Decimal
constructor from the decimal module.
>>> from decimal import Decimal # Need to import
>>> a = Decimal('0.1')
We can check for earlier operations.
>>> 0.1 + 0.1 + 0.1 == 0.3
False # Inexact addition
>>> a + a + a == Decimal('0.3')
True # Exact
As you can see, the Decimal
type in Python lets us do exact addition in Python.
The decimal module also incorporates a notion of significant places. To preserve significance, the coefficient digits do not truncate trailing zeros.
>>> 1.30 + 1.50
2.8 # Discards 0
>>> Decimal('1.30') + Decimal('1.50')
Decimal('2.80') # Retains 0
>>> 2.60 * 2.50
6.5 # Discards 0
>>> Decimal('2.60') * Decimal('2.50')
Decimal('6.5000') # Four significant 0s
Contrary to the built-in floating-point arithmetic in Python, the Decimal type works how people expect it to work. This Decimal Arithmetic Specification on which the type is based on states that
... based on a floating-point model which was designed with people in mind, and necessarily has a paramount guiding principle – computers must provide an arithmetic that works in the same way as the arithmetic that people learn at school.
We can construct decimal instances from integers, strings, or floats. Construction from an integer or a float performs an exact conversion of that integer or float value.
>>> Decimal(1) # Decimal object from integer
Decimal('1')
>>> Decimal(0.5) # Decimal object from float
Decimal('0.5')
>>> Decimal(str(2**5)) # Decimal object from string
Decimal('32')
Earlier, to get higher precision of the float 0.1, we multiplied the stored fraction with $10^{55}$. We can create a decimal object from a floating-point.
>>> from decimal import Decimal
>>> a = Decimal.from_float(0.1) # Convert using built-in method
>>> a
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(0.1) # Convert directly from a float
We can see that the exact conversion of 0.1 to decimal results in the actual value that Python interprets for the float.
We can always assign a new name to a function and refer to it. In Python, we can also rename a function import from a module. Thus, we can name Decimal as D
for convenience.
>>> from decimal import Decimal as D
>>> D('0.1') # Creating objects using name `D`
Decimal('0.1')
In the above listing, we are renaming the Decimal()
function directly at the time of import. This is also the recommended way to avoid name-clashes in Python.
Python implements both binary and decimal floating-point in terms of published standards. While the built-in float type exposes only a small portion of its capabilities, the decimal
module exposes all essential parts of the standard.
For instance, the decimal
module exports an object getcontext()
, which has several configurations in place which you can modify.
>>> from decimal import Decimal as D, getcontext
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, FloatOperation, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])
Right now, the precision is 28 (prec=28
) stored in the context object. Let's change it to 4
.
>>> D(1)/D(3)
Decimal('0.3333333333333333333333333333') # 28 digits
>>> getcontext().prec = 4
>>> D(1)/D(3) # 4 digits
Decimal('0.3333')
For calculations in accounting and financial transactions, you might want tighter control on precision and rounding off values. You can check additional details in the python documentation for the decimal
module.
Decimal
types and Floating-point
numeric types in Python. Can you summarise the main difference between these two numeric types?
The Decimal
numeric types have some methods available to them.
You can check the complete list of methods available using the help(Decimal)
statement on the Python Interpreter after importing it from the decimal
module.
Some methods available in the decimal object are shown in the table 3.
Method | Description |
---|---|
as_integer_ratio() |
Return a pair (n, d) of integers that represent the given Decimal instance as a fraction, in lowest terms and with a positive denominator. |
from_ float()`` |
Converts a float to a decimal number, exactly. |
exp() |
Return the value of the (natural) exponential function e**x at the given number. |
normalize() |
Normalise the number by stripping the rightmost trailing zeros and converting any result equal to Decimal('0') to Decimal('0e0') . |
sqrt() |
Return the square root of the argument to full precision. |
to_integral_value() |
Round to the nearest integer |
The below code sample shows the usage of these methods.
>>> Decimal('1.3').as_integer_ratio() # Fraction
(13, 10)
>>> Decimal.from_float(0.3) # Create Decimal from float
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(1).exp() # e**1
Decimal('2.718281828459045235360287471')
>>> Decimal('144').sqrt() # Square root
Decimal('12')
>>> Decimal('12.5333').to_integral_value() # Get the nearest integer
Decimal('13')
We can check additional methods and their description in the Python documentation.
What is the value of the following expression?
>>> Decimal.from_float(10).sqrt().to_integral_value()
- 3
- 2
- 10
- Raises Error
Fractions
Another numeric type that we often use in the real world is fractions.
In Python, the fractions module provides support for rational number arithmetic.
To create a fraction object in Python, we will first need to import the Fraction
function or constructor from the fractions
module.
>>> from fractions import Fraction
You can construct a fraction number from a pair of integers, another fraction number, decimal object, float, or string. The fraction returns the numerator and denominator in the lowest form.
>>> from fractions import Fraction
>>> Fraction(15, -6) # From pair of integers
Fraction(-5, 3)
>>> Fraction(0.5) # From float
Fraction(1, 2)
>>> Fraction() # Without arguments
Fraction(0, 1)
>>> Fraction('7e-6') # From string
Fraction(7, 1000000)
>>> from decimal import Decimal
>>> Fraction(Decimal('3.3')) # From Decimal Object
Fraction(33, 10)
Let's check the value of float 0.1
in the form of a Fraction.
>>> Fraction(0.1) # Will return fraction used by python
Fraction(3602879701896397, 36028797018963968)
>>> Fraction(Decimal('0.1')) # Will return fraction 1/10
Fraction(1, 10)
You can see that the fraction returns the exact fraction approximate used to represent 0.1
float in Python.
Like other numeric types, Fractions are immutable. The numerator and denominator are available as attributes to the fraction object.
>>> a = Fraction(1, 10)
>>> a.numerator
1
>>> a.denominator
10
What do you think is the result of the following expression?
>>> from fractions import Fraction
>>> Fraction(1, 10) + Fraction(9, 10)
1
Fraction(11, 10)
Fraction(1, 1)
- Raises Error
Fractions expectedly interact with the arithmetic operators.
You can add, subtract and multiply fractions with numbers, and it returns the result in the form of a Fraction
object. Let's add two fractions.
>>> Fraction(1, 10) + Fraction(9, 10) # Addition
Fraction(1, 1)
The fractions object is always returned in the lowest form.
>>> Fraction(7, 6) - Fraction(4,6 ) # Subtraction
Fraction(1, 2)
This also applies to multiplication.
>>> Fraction(1, 10) * 5 # Multiplication
Fraction(1, 2) # Lowest Form
And because Python does not store exact fractions in floats, it is recommended to create fractions from Decimal
objects.
>>> Fraction(11,10 ) + Fraction(Decimal('0.9'))
Fraction(2, 1)
You can view more methods of fractions in the documentation for the fractions module.
What do you think is the result of the following expression?
>>> (Fraction(2, 1)*Fraction(1, 2)) + Fraction(Decimal('0.5'))
Fraction(3, 2)
Fraction(1, 1)
Fraction(1.5)
1.5
Complex Numbers
The last numeric type that we will look at is complex numbers in Python.
A complex number is a number that can be expressed in the form a + bi
, where a and b are real numbers, and i
is a solution of the equation x² = −1
. Because no real number satisfies this equation, i
is called an imaginary number.
$$
z = \overbrace{ \underbrace{a}\text{real} + \underbrace{ib}\text{imaginary} }^\text{complex number} \tag{10}
$$
Complex numbers can be constructed using a complex
constructor.
>>> complex(3, 4) # Using constructor
3 + 4j # Complex Number
>>> 3 + 4j # Imaginary Literal
3 + 4j # Complex Number
Some of the the attributes of the complex numbers are :
>>> z = 3 + 4j
>>> z.real # Real Part of Complex Number
3.0 # type: <class 'float'>
>>> z.imag # Imaginary part of Complex Number
4.0 # type: <class 'float'>
The complex conjugate of a complex number is the number with an identical real part and an imaginary part equal in magnitude but opposite in sign. For example, if a and b is real, then the complex conjugate of $a + bi$ is $a - bi$.
We can write a function that takes a complex number as input and returns its conjugate.
>>> def conjugate(comp):
return complex(comp.real, -comp.imag)
We can use the above function in the following way.
>>> a = complex(1, 2)
>>> b = conjugate(a)
>>> b
(1.0, -2.0)
Fortunately, a complex number has a built-in method conjugate()
that gives the conjugate of any complex number.
>>> z = 3 + 4j
>>> z.conjugate()
( 3 - 4j)
The magnitude of a complex number is the square root of the sum of the square of its real and imaginary parts.
$$
\text{Magnitude of z} = \sqrt{a^2 + b^2} \tag{11}
$$
In Python, we can get the square root of any number using the sqrt()
from the math
module. We can write a function that returns the magnitude of a complex number in the following way.
>>> import math
>>> def magnitude(comp):
return math.sqrt(comp.real**2 + comp.imag**2)
So, we can use our custom defined function as:
>>> z = 3 + 4j
>>> magnitude(z)
5.0
You can use the built-in Python's abs()
to get the magnitude of a complex number. The abs()
function works similarly as to our magnitude()
function we defined above.
>>> z = 3 + 4j
>>> abs(z) # math.sqrt(a**2 + b**2 )
5.0
For performing calculations using complex numbers, python provides the cmath
module. You can check out the Python documentation to know more about it in detail.
What do you think is the output of the following expression?
>>> z1 = 3+4j
>>> z2 = 3-4j
>>> type(abs(z1 + z2))
<class 'float'>
<class 'int'>
<class 'complex'>
<class 'decimal'>
In the next section, we will look into collections type objects that are used to store other data types.
Collections
Let's look into container objects available in Python.
We earlier encountered lists
, tuples
and dictionaries
.
The Collection types act as a container for storing references to other objects. We can classify the built-in python containers into three groups as shown in table 4.
Container | Examples |
---|---|
Sequences Types | Lists , Tuples , Strings |
Set Types | Sets and Frozen Sets |
Mapping Types | Dictionary |
There are many more collection type objects present in the Python standard library. For now, we will look into the ones which we are most likely to use.
Sequence Types
Let's start with sequence types. Python objects stored in the sequences are ordered sequentially.
Sequentially ordered objects mean that Python sequentially assigns each object a unique position or index. The objects can be accessed using their position index in the sequence.
The three basic sequence types are lists, tuples, and strings. Sequences are built-in containers that store the ordered set of Python objects.
Ordered sets of objects can be accessed using the format s[i]
where s
is the sequence name while i
is the object's index. Accessing an item in a container object using an index is called indexing. The index starts from $0$ for the left-most item and increases by one as it moves towards the right. If you use a negative integer, Python will start from the rightmost element with the last element being $-1$, the second last being $-2$, so on.
We have already encountered strings, lists, and tuples. Let's look at them in more detail.
>>> a = ["Eena", "Meena", "Deeka"] # List Sequence
>>> a[0] # Accessing first element
'Eena'
>>> b = ("Daai", "Daam", "Nika") # Tuple Sequence
>>> b[-1] # Accessing last element
'Nika'
>>> c = "Maaka naaka naaka" # String Seqeunce
>>> c[3] # Accessing fourth element
'k'
What does the following expression evaluate to?
>>> z = [0, 1, 2, 3, 4, 5]
>>> z[2]
- 1
- 2
- 3
- 4
z = [1, 2, 3]
.
And you run the command z[100]
using the Python interpreter. How do you think Python will respond?
Python raises an IndexError
error with the message list index out of range
. You can think of an index as a reference that can be used to access the object. Let's check with an example.
We will create two string objects and check their reference counts.
>>> from sys import getrefcount
>>> first_ship, second_ship = "Going Merry", "Thousand Sunny"
>>> getrefcount(first_ship)
2
To recall, that getrefcount()
function provides the number of the references to a given object. The number returned is one greater than actual as the function call temporarily references the object.
Now, let's put both of our ships into a list container and check the reference count.
>>> ships = [first_ship, second_ship]
>>> ships[0]
'Going Merry'
>>> getrefcount(first_ship)
3
The reference count increased as we have another way to reference the string object using the list position index.
Another thing to keep in mind is that the index can change if the container's content is changed.
To understand, let's take the following example where we delete an element of the list. We can delete an element of a list by using the del
keyword.
>>> a = [0, 1, 2, 3, 4]
>>> a[2]
2
>>> del a[2] # delete the element at index 2
>>> a
[0, 1, 3, 4]
>>> a[2]
3
The index 2
, which was earlier assigned to number 2
, is reassigned to integer 3
after integer 2
is removed from the sequence.
What do you think is the final value of the name z
after the following operation?
>>> z = "Loud Musiic"
>>> del z[9]
Loud Muiic
Loud Music
Loud Musii
- Raises
TypeError
Unlike list objects, strings
and tuple
objects cannot be modified once created. Whether an object can be modified after creation relates to mutability. Next, let's look into the mutability of sequences.
Mutability
Immutable sequences are containers whose values don't change after we create them. Strings and Tuples are immutable sequences, while List objects are mutable sequences.
We can assign items in a list object to a different value after it's creation.
>>> a = [10, 30, 30, 40] # List object created
>>> a[1] = 20
>>> a
[10, 20, 30, 40] # List object modifed
We cannot modify items in a Tuples sequence.
>>> b = (1, 2, 3)
>>> b[1] = 2 # Cannot modfiy a tuple sequence
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
Similarly, Strings also cannot be modified once created.
>>> c = "Hxllo World"
>>> c[1] = "e" # Cannot modify a string sequence/literal
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
Immutable sequences such as tuples and strings can be hashed or used as keys for dictionaries. In contrast, mutable sequences such as lists cannot be used.
>>> a = { "name": "Foo", (1, 2) : "Bar" }
>>> a[(1,2)] # Using a tuple as dictionary key
'Bar'
>>> a["name"] # Using a string as dictionary key
'Foo'
Storing tuples as dictionary keys are useful in certain cases. Python will allow it as long as each element of a tuple is immutable.
If a tuple has a mutable element, Python will raise TypeError
.
>>> a = {(1, []) : "Foo"} # Empty list is mutable
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
The immutable objects are safer to pass around, knowing that they cannot be modified. On the other hand, mutable objects can be modified via any of their references, leading to an incorrect result.
Also, Python has many optimizations in place for immutable objects, which makes working with them, a lot faster. That's all you need to know about immutability for now.
Common Operations for Basic sequences
There are several common operations applicable to the basic sequences.
Let's look into sequences in a bit more detail.
Operation | Syntax | Description |
---|---|---|
Indexing | s[i] |
Returns item stored in i th index |
Membership | x in s or x not in s |
True if an item of s is equal to x, else False |
Concatenation | s + t |
Adds up two equal type sequences |
Replication | s * n or n * s |
Equivalent to adding s to itself n times |
Length | len(s) |
Returns the total number of items |
Slicing | s[start : stop : step] |
Returns a slice of items from the sequence with step |
Counting | s.count(x) |
Returns the total instances of item x in s |
Searching | s.index(x,[,i j]]) |
Returns the index of first instance x in s |
Comparison | s1 < s2 or s1 == s2 |
Lexicographically comparison of corresponding items |
Min-Max | max(s) or min(s) |
Smallest or largest item of s |
Sorting | sort() |
Sorts the elements in ascending or descending order |
Any-All | any(s) or all(s) |
True if any/all items are True |
Sum | sum(s) |
Returns the sum for a sequence of integers |
Unpacking | x,y,z = s |
corresponding item can assigned using unpacking |
We already looked at Indexing operation. Let's look into other operations with examples.
We can use the membership operation.
Membership
The x in s
operator tests whether the object x
is present in the sequence s
. For strings, in
and not in
operators accept substrings.
>>> 1 in (1,2,3) # Checking containment in Tuple
True
>>> 0 not in [1,2,3,0] # Checking containment in List
False
>>> 'gre' in 'great kindness' # Checking containment in String
True
We can use the search or index operation to get an element's position in a sequence. Python provides a built-in method index()
for retrieving the index of the first occurrence inside the sequence if it exists. If the element is not present inside the sequence, Python raises ValueError
.
Let's take an example.
>>> a = (1, 2, 1, 3, 2, 4, 5, 2)
>>> a.index(2)
1
The index()
method also takes optional arguments i
and j
for the start and end index of the sequence's` to search, which is similar to searching the sequence's sub-sequence.
Therefore, the statement s.index(x, i, j)
is equivalent to s[i:j].index(x)
.
>>> a = [1, 2, 1, 3, 2, 4, 5, 2]
>>> a.index(2, 5) # Start search from 5th item
7
>>> a.index(2, 2, 5) # Start search from 2nd item till 5th item
4
If no element exists inside the sequence, the index
method returns ValueError
.
>>> "Team".index("I") # There is no 'I' in 'Team'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: substring not found
We can get the total number of items in a sequence obtained using the built-in function len()
. Let's see a couple of examples.
Let's start with tuples
.
>>> a = (1,2,3) # Tuple
>>> len(a)
3
Similarly, we can get the length of lists
and strings
.
>>> b = [] # List
>>> len(b)
0
>>> first_name = "Rochinate" # String
>>> len(first_name)
9
Indexing in Python starts at 0
. We can get the index of the last element in a sequence using the expression: len(s) - 1
. We can also use -1
to access the same.
We can use concatenation or replication to create new sequences from sequences. Concatenation is the operation of linking elements together in series. Let's look at both of them in detail.
Concatenation
The same type of sequences can be concatenated to form a new objects.
>>> (1,2) + (3,4,5) # Concatenating two tuples
(1,2,3,4,5)
>>> [1,2] + [3,4,5] # Concatenating two lists
[1,2,3,4,5]
>>> "1 2 3 " + "4 5" # Concatenating two strings
'1 2 3 4 5'
Concatenating immutable sequences always results in a new object.
Replication
When you multiply a number n
greater than 0
to sequence, it is equivalent to adding the sequence to itself n
times.
>>> 5 * (1,2) # Replicating a tuple
(1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
>>> [1,2,3] * 3 # Replicating a tuple
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> "Da-da Na-da " * 3 # Replicating a tuple
'Da-da Na-da Da-da Na-da Da-da Na-da '
When you multiply a sequence with 0
, it becomes an empty sequence.
>>> (1,2,3) * 0
()
Values of n less than 0 are treated as 0 and yields an empty sequence of the same type as s.
>>> (1,2,3) * -5 # Replicating with a negative number
() # Empty Tuple
What is the output of the below Python expressions?
>>> a = [1,2,3]
>>> b = [a]
>>> c = 4 * b # 4 copies of elements of b
>>> c
[[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]
>>> a[0] = 100
>>>> c
[[100, 2, 3], [100, 2, 3], [100, 2, 3], [100, 2, 3]]
[[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[4, 8, 12], [4, 8, 12], [4, 8, 12], [4, 8, 12]]
- Raises
ValueError
The replicated elements here are shallow copies of sequence. If you change the reference element, the change will be reflected across all repeated elements.
To better understand, let's take an example.
>>> a = [1,2,3]
>>> b = [a]
>>> c = 4 * b # 4 copies of elements of b
>>> c
[[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]
>>> a[0] = 100
>>>> c
[[100, 2, 3], [100, 2, 3], [100, 2, 3], [100, 2, 3]] # All items will change
So, keep in mind that replicating a sequence results in shallow copies of the elements. Next, we will look at the slicing operation in sequences.
Slicing
As sequence types contain ordered items, the slicing operation returns a set of items within the defined starting index and ending index.
$$
\text{Slice Operation} \rightarrow s[i : j : k] \tag{12} \where, \text{i is start index, j is end index and k is the step}
$$
For example, for a tuple s
with value (0,1,2,3,4,5,6,7,8,9)
,
$$
s[3:7:1] = \left( \overbrace{ 0, 1, 2, \underbrace{3, 4, 5, 6}_\text{slice} , 7, 8, 9 }^\text{indices} \right) = (3, 4, 5, 6) \ \\tag{13}
$$
The slicing operation returns a set of items from the start index to the end index, not including the end index. The step dictates which elements to take after the first one. By default, the step is one that means the slice returns consecutive elements. In contrast, for step 2, the slicing operation will return every alternate item with a start and end index.
When we use the optional step argument, other than 1, to slice a list, it is called an extended slice.
Figure 2 shows various slices of the string sequence PRIMER
.
We can test a few examples of slicing operations in Python.
>>> (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)[3:7] # Slicing with step 1
(3, 4, 5, 6)
>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9][0:7:2] # Slicing with step 2
[0, 2, 4, 6]
>>> "IansNinsDasdIasaAas"[0:-1:4] # Slicing with step 4 and negative index
'INDIA'
What is the result of the following slicing operation?
>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[::2]
[0, 2, 4, 6, 8]
[1, 3, 5, 7, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 3, 5, 7]
In the exercise above, list a
starts from 0, skips element, picks 2
, skips element, picks 4
, and so forth. Slicing can seem complicated and un-intuitive sometimes. Let's understand more in detail.
Step can be understood in the following way. When you wish to get the slice s[i:j:k]
of a sequence s
, python returns the items s[i], s[i + k], s[i+2*k], ... , s[i + n*k]
where n*k
is less than the length of the seqeunce len(s)
.
So, for a list s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
, with the slice s[0:7:2]
, python returns the items s[0], s[2], s[4], s[6]
.
The slicing operator s[i:j]
extracts a subsequence from s
consisting of the elements with index k, where i <= k < j
. If Python finds no such elements satisfying the condition, it returns an empty sequence of the same type.
The steps can also be negative.
- If the
step
is positive and if the starting indexi
is omitted, Python assumes the start index to be 0. - If the
step
is positive and if the ending indexj
is omitted, Python assumes the end index to the length of the sequence len(s) - If the
step
is negative and if the starting indexi
is omitted, Python assumes sets the starting index to the length of the sequence orlen(s)
. - If the
step
is negative and if the ending indexj
is omitted, Python sets the end index to0
.
Few examples of slicing are shown in the below code listing.
>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[::2]
[0, 2, 4, 6, 8]
>>> a[::-2]
[9, 7, 5, 3, 1]
>>> a[5::-1] # Negative Index
[5, 4, 3, 2, 1, 0]
>>> a[3:5:-1]
[]
What's the value of the following expression?
>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[5::-1]
[5, 4, 3, 2, 1, 0]
[4, 3, 2, 1, 0]
[6, 5, 4, 3, 2, 1, 0]
[]
Counting
To count the number of occurrences of an element in a sequence, we can use the built-in count()
method.
For strings, the count()
method can also take a substring. If the item doesn't exist, the count()
method simply returns 0
. Let's take a look at some examples.
Let's create a tuple of elements.
>>> a = (0, 1, 2, 2, 2, 5, 6, 2, 8, 9)
>>> a.count(2)
4
If the item doesn't exist, the count()
method simply returns 0
.
>>> a.count(99)
0 # No element exists
For lists
, it works similarly.
>>> [1,1,1,3,4,5].count(1)
3
For strings, the count()
method can also take a substring.
>>> "This is a very long sentence".count('e')
4
>>> "This is a very long sentence".count('is')
2
What is the output of the following Python code?
>>> "Mississippi".count("is")
- 0
- 1
- 2
- 3
Comparison
In an earlier chapter, we saw that we could compare string literals lexicographically.
Lexicographic order is ordering words based on the alphabetical order of their component letters.
Python lexicographically compares primitive sequences against other sequences. Let's take a look at how this works.
Lexicographical comparison between built-in collections works as follows:
For two collections to compare equally, they must be of the same type and same value.
>>> [1, 2, 3] == [1,2,3]
True
>>> (0, 0.1, 0.2) == (0, 0.1, 0.2)
True
Collections that support order comparison have the same order as their first unequal elements. For example, [1,2,a] <= [1,2,b]
has the same value as a <= b
.
>>> [1, 2, 80] > [1, 2, 81]
False
>>> (9, 8, 7) > (9, 8, 0)
True
If a corresponding element does not exist, the shorter collection is ordered first
>>> [1, 2] < [1, 2, 9]
True
In the following code, what is the value of (A, B, C)
?
>>> (7,) > (1,2)
(A)
>>> [1] == [1,0]
(B)
>>> "Hello" < "hello"
(C)
True, True, True
True, False, True
True, False, False
False, False, False
Min-Max
Previously, we saw that basic sequences could be compared against the same type of sequences. The built-in functions min()
and max()
allows the user to get the least or the maximum value of the item stored in a sequence. Let's take a look at some examples.
Let's create a tuple of numbers.
>>> a = (1, 2, 3, 4, 5, 6, 7, 8)
Now, let's call max()
and min()
on the tuple to get the maximum value and minimum value correspondingly.
>>> a = (1, 2, 3)
>>> max(a)
3
>>> min(a)
1
For the functions min()
and max()
, all the items should be of the same type.
>>> b = [1, "Hello", (1,2,3)]
>>> max(b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'str' and 'int'
The maximum or minimum value for a string returns the character having the highest or lowest character value.
>>> max("Hello World")
'r'
>>> min("Hello World")
' '
>>>
What is the value of the following Python code?
>>> max([(1, 2, 3), (1,), (1, 2, 4), (1, 3, 1) ])
(1, 2, 3)
(1,)
(1, 2, 4)
(1, 3, 1)
Sorting
Often you would require to sort the list in ascending or descending order of item values. Python provides a built-in function sorted()
to help you do just that.
The sorted()
creates a new list object from a sequence in which Python sorts items in ascending or increasing order.
>>> a = (100, 20, .3, -0.123 ) # Tuple
>>> sorted(a)
[-0.123, 0.3, 20, 100] # A new sorted List object in ascending order
The sorted()
function takes an optional keyword argument reverse with the default value False
. To get sorted items in descending order, you can include the keyword argument reverse=True
.
>>> b = ( 11, 2, 355, 123 ) # Tuple
>>> sorted(b, reverse=True)
[355, 123, 11, 2] # Sorted item in descending order.
For strings as arguments, the sorted()
function returns a lexicographically ordered list of characters.
>>> sorted("Hello World")
[' ', 'H', 'W', 'd', 'e', 'l', 'l', 'l', 'o', 'o', 'r']
For the sorted function to work, all the containing items in a sequence should be of same types of items which can be compared against other stored items.
>>> sorted([(), (1,2), (1,2,3), (1,23)]) # List of Tuples
[(), (1, 2), (1, 2, 3), (1, 23)]
For instance, the sorted()
function will raise TypeError
if you use a mix of strings and integers as we cannot compare them directly.
>>> sorted([1,2,3,"sas"])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'
What's the output of the following expression?
>>> type(sorted("CABBAGE"))
<class 'list'>
<class 'str'>
<class 'tuple'>
<class 'int'>
Any-All
Earlier, we mentioned that in Python, we could test every object for truth value. There are many programming cases where you would like to check if any item has a truth value True
or if all items have a truth value True
. For that purpose, python provides built-in functions any()
and all()
.
>>> a = [0, 0, 0]
>>> any(a) # Check if any of the items in a has truth value ``True``
False
>>> b = ("", [], ()) # Empty sequences have truth value `False`
>>> any(b)
False
>>> c = [1,2,0]
>>> all(c) # Check if all items in `c` have truth value ``True``
False
>>> d = (100, 200, 300)
>>> all(d)
True
What's the output of the following expression?
>>> e = ("", [], ())
>>> any(e)
True
False
As you might recall, all empty sequences have the truth value, False
. You can verify by checking the result of bool("") or bool([])
in the interpreter. Therefore, the above exercise results in False
.
Another operation that is often used with lists is summing up all of its elements. Let's look into it next.
Sum
Python contains a built-in function sum()
to get the sum of numeric items in a sequence. The sum()
start and the items of a sequence from left to right and returns the total.
For instance,
>>> a = [1, 2, 3, 4, 5]
>>> sum(a) # Sum all the elements of list `a`
15
We can do the same for a tuple
.
>>> b = (0.3, 0.4, 0.5)
>>> sum(b)
1.2
The sum()
requires each item type to support the addition operation +
with other item types. The sum()
function doesn't support string sequences. If you try to add strings, it will show a TypeError
.
>>> c = ["Hi", "There"]
>>> sum(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
. But we didn't provide any integers to add. Can you guess why Python prints such a message?
When we sum all the list elements, we usually assume a starting value, usually called accumulator. This value is usually set at 0,
and we keep elements adding to it. And when the list is exhausted, the accumulator's final value is returned as the total value.
The sum()
function allows you to modify the accumulator. It takes an accumulator argument start
whose default value is 0. You can initialize the sum()
to start from other numbers as well.
>>> c = [0.3, 0.6, 0.9]
>>> sum(c, 10) # add the sum of `c` list to 10
11.8
You can also sum tuples if you use a different starting value.
>>> e = [(3, 4), (5, 6)]
>>> sum(e, (1, 2))
(1, 2, 3, 4, 5, 6)
However, note that you still cannot add strings using the sum()
function.
Unpacking
Next, we come to the unpacking of sequences.
When we put objects into containers, it can be thought of as packing objects inside a container. If we wish to refer to each item in a container, we can use indexing to access via their position.
Python provides a shorter syntax to assign to individual items in a stored container. This is referred to as unpacking sequences.
Let's create a list with tuples.
>>> a = [(1,2,3), (4, 5,6), (7,8,9)]
Now, we can obviously name individual tuple
items using the index. For instance,
>>> first = a[0] # (1,2,3)
>>> second = a[1] # (4,5,6)
>>> third = a[2] # (7,8,9)
However, Python provides a much concise way to extract and assign individual items in a container.
>>> first, second, third = a
>>> first
(1,2,3)
As I mentioned before, unpacking works when there is an equal number of names corresponding to the sequence items.
>>> a, b, c = (1, 2, 3, 4) # Won't work
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 3)
You can also unpack characters from a string sequence.
>>> a, b, c, d, e = "hello"
>>> a
'h'
Next, we will look into Python lists in more detail.
Lists
In the previous section, we saw the operations that are common to all the sequences. We will now look at operations for lists.
list
and tuple
?
In Python, built-in list
objects are mutable while tuple
is immutable. This difference leads to several operations allowed in list
while not permissible in tuple
. Let's look at the list
first.
In Python, you can use a pair of square brackets or use the list()
constructor to create lists. The list constructor can take other sequences to create a list object.
>>> a = (1, 2, 3) # Tuple iterable
>>> list(a) # Creating a list from a tuple
[1, 2, 3]
As strings are also iterables, we can construct lists from strings.
>>> b = "Hello" # String iterable
>>> list(b) # Creating a list from a string
['H', 'e', 'l', 'l', 'o']
Now let's look at some of the methods and operations of List object available in Python.
Operation | Syntax | Description |
---|---|---|
Item assignment | s[i] = x |
Item at i of s is replaced by x |
Slice assignment | s[i:j:k] = t |
Slice of s from i to j is replaced by sequence t |
Item Deletion | del x[i] |
Removes the item at i |
Slice Deletion | del s[i:j:k] |
Removes the items of slice from the list |
Append | s.append(x) |
Appends x to the end of the sequence |
Insertion | s.insert(i, x) |
Inserts x into s at the given index i |
Clear | s.clear() |
Removes all the items from s |
Copy | s.copy() |
Creates a shallow copy of s |
Extend | s.extend(t) |
Extends s with the contents of sequence t |
Pop | s.pop([i]) |
Retrieves the item at i and removes it from s |
Reverse | s.reverse() |
Reverses the items of s |
Remove | s.remove(x) |
Remove the first item from s where s[i] is equal to x |
Replication | s *= n |
Updates s with its content repeated n times |
Sorting | s.sort() |
Updates s with sorted list of items |
Assignment and Deletion
Lists are mutable container objects which can be modified. We can modify the items directly by accessing the item using indexing and reassigning using the assignment operator (=
).
>>> a = [1, 3, 3]
>>> a[1] = 2 # Modify the second item
>>> a
[1, 2, 3]
Similar to Item modification, a slice of list object s can also be reassigned to other sequences t using the form,
$$
s[i:j] = t
$$
The slice assignment will replace the contents of the slice object with the contents of sequence t
.
>>> a = [10, 2, 3, 4 50, 60]
>>> a[1:4] = [20, 30, 40] # the step `k` in slice is default 1
>>> a
[10, 20, 30, 40, 50, 60]
We can replace the contents of the slice with any arbitrary length sequence. It doesn't need to be the same as the slice length.
>>> c = [1, 5, 6]
>>> c[1:3]
[5,6] # slice to be replace
>>> c[1:3] = [2, 3, 4, 5, 6, 7]
>>> c
[1, 2, 3, 4, 5, 6, 7]
Let's do a quick exercise to check if you understand the slice assignment. What's the output below?
>>> z = [3, 6, 9, 11, 14, 17, 21]
>>> z[3:6] = [12, 15, 18]
>>> z
[3, 6, 9, 11, 14, 17, 21]
[3, 6, 9, 12, 15, 18, 21]
[3, 6, 9, 11, 15, 18, 17, 21]
[3, 6, 9, 12, 15, 18]
So far, we have been assigning slices to the list. We can also assign content from sequences other than lists.
>>> b = [1, 5]
>>> b[0:1] = (1, 2, 3, 4) # Contents from sequence
>>> b
[1, 2, 3, 4, 5]
We can do the same with string sequences as well.
>>> c = ["g","t"]
>>> c[1:2] = "reat"
>>> c
['g', 'r', 'e', 'a', 't']
If you wish to insert two or items replacing a single item, using a slice will be the best approach.
>>> b = [1, 40, 50, 60, 70]
>>> b[0:1] = [10, 20, 30]
[10, 20, 30, 40, 50, 60, 70]
Slices with a step argument other than the default step 1
are called extended slices.
To assign to an extended slice, the length of the list to be assigned must be equal to the slice object's length. For instance,
Let's take a list.
# We would like to modify the 3rd, 5th, and last item
>>> c = [1, 5, 1, 9, 1, 13, 1]
Now, we wish to modify the 3rd, 5th, and the last item of the list. We can get the slice using a step size 2
. Let's check if we can get our desired slice.
>>> c[2::2] # Get the slice object
[1, 1, 1]
Now that we have our desired slice let's assign it to our desired sequence.
>>> c[2::2] = [7, 11, 15] # Slice Assignment
>>> c
[1, 5, 7, 9, 11, 13, 15]
If you try to assign a sequence with a length different from the slice objects, Python will raise ValueError.
>>> c[2::2] = [7, 11, 15, 17]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: attempt to assign sequence of size 4 to extended slice of size 3
What is the value of the following statement?
>>> d = [1, 4, 3, 9]
>>> d[1::2] = [2, 4]
>>> d
[1, 2, 3, 4, 9]
[1, 2, 3, 4]
[1, 2, 4, 3, 4]
[2, 4, 3, 9]
Deletion
To delete multiple items, we can slice a list object and use the del
keyword or assign it to an empty sequence. Let's take a look at how we can do that.
>>> a = [1, 2, 3, 7, 4, 5, 6]
>>> del a[3] # Delete the 4th item
>>> a
[1, 2, 3, 4, 5, 6]
We can delete the slice object using either the del
keyword or assigning the slice to an empty list.
>>> b = [1, 19, 7, 5, 6, 2, 3, 4]
>>> b[1:5] # slice needed to be deleted
[19, 7, 5, 6]
>>> del b[1:5] # Same as b[1:5] = []
>>> b # list object modified
[1, 2, 3, 4]
Therefore, you can delete the slice object using either using the del
or assigning the slice to an empty list.
However, to delete an extended slice, we can use only the del
keyword.
>>> c = [1, 3, 2, 4, 3, 5, 4, 7, 5]
>>> c[1::2] # slice needed to be deleted
[3, 4, 5, 7]
>>> del c[1::2] # Delete the slice
>>> c # List object modified
[1, 2, 3, 4, 5]
What is the final value of d
in the following code?
>>> d = [1, 2, 3, 4, 5]
>>> d[1:4] = [5, 4, 3, 2]
>>> del d[2::2]
[1, 5, 3, 5]
[1, 2, 4]
[1, 2, 3]
[1, 5, 4, 2]
Append, Insertion, and Extend
If you wish to add items to a list, you can use the +
operator to concatenate two lists. However, concatenation leaves the two lists unchanged. We can understand this in the code below.
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> a + b
[1, 2, 3, 4, 5, 6]
>>> a
[1, 2, 3]
To modify the list object, Python provides a built-in method append()
for the list object, which adds items at the end of the list.
>>> c = [1, 2]
>>> c.append(3)
>>> c
[1, 2, 3] # list modified
What is the output from the following statements?
>>> d = [1, 2, 3]
>>> d.append((1, 2))
- Raises
TypeError
[1, 2, 3, 1, 2]
[1, 1, 2]
[1, 2, 3, (1, 2)]
The append()
method takes only a single argument; therefore, it can add only a single object at the end of the list. To add multiple values, lists have a built-in method extend()
, which adds another sequence's contents at the end of the list.
>>> d = [1, 2, 3]
>>> d.extend((1, 2)) # Add contents from the tuple to the list
>>> d
[1, 2, 3, 1, 2] # List modified
The s.extend(t)
method is the same as using the augmented assignment operator +=
for concatenation. For instance, we can extend a list using the following method.
>>> d = [1, 2, 3]
>>> d += [1, 2] # Same as d = d + [1, 2]
>>> d
[1, 2, 3, 1, 2]
We can extend the list from various sequences.
>>> c = [1, 2, 3]
>>> c.extend([4, 5, 6]) # Similar as c += [4,5,6]
>>> c
[1, 2, 3, 4, 5, 6]
>>> c.extend((7, 8)) # Extending from Tuples
>>> c
[1, 2, 3, 4, 5, 6, 7, 8]
>>> c.extend("NO") # Extending from Strings
>>> c
[1, 2, 3, 4, 5, 6, 7, 8, 'N', 'O']
There is a caveat that you need to know about the extend
operation and concatenation. To understand this, let's do an exercise.
What is the output of the following operation?
>>> x = y = z = [1, 2] # All names point to same list object
>>> x += [3, 4]
>>> y = y + [3, 4]
>>> x is y, x is z, z
(True, True, [1, 2, 3, 4])
(False, True, [1, 2, 3, 4])
(False, True, [1, 2])
(True, True, [1, 2])
Augmented assignment operator +=
along with append()
and extend()
modifies the same object. However, if we use the expanded form of +=
, the name is reassigned to a different object.
Let's take a list object a
.
>>> a = [1, 2, 3]
>>> id(a)
139958920005384
Let's modify the list using the augmented assignment operator (+=
).
>>> a += [4] # Add elements
>>> a
[1, 2, 3, 4]
>>> id(a)
139958920005384 # Same object
Now, let's modify the object using the expanded form of the assignment operator.
>>> a = a + [5]
>>> a
[1, 2, 3, 4, 5]
>>> id(a)
139958920005128 # Different object
This discrepancy can be explained by the way the operators work behind the scene.
To recall, Magic or Special methods allows objects to use various operators such as +
, -
, +=
, etc. Whenever you use an operator such as +
to add two objects, Python executes the function __add__()
behind the scene for the object.
The +
operator uses the __add__
magic method, which doesn't modify any operands. Therefore, the expression, s = s + [a], returns a new object and is reassigned to the name.
On the other hand, the +=
operator calls the __iadd__
magic method that modifies the arguments in-place. This is a tricky bit of information you should be keeping in mind.
For lists, it is easier to remember that+=
is a shorthand forlist.extend()
to avoid such erroneous behavior.
In-place methods modify the objects which call it. For example, when we call when we append an element to a list, the list object is modified.
>>> a = [1, 2]
>>> a.append(3)
>>> a # list modified in-place
[1, 2, 3]
While non-in-place methods don't modify their object. Instead, they return a copy of that object.
>>> name= "luffy"
>>> name.upper() # returns a copy
'LUFFY'
>>> name
'luffy'
Both append()
and extend()
methods insert items at the end of the list.
One method we can use is the slice assignment. As the slice assignment removes the item at the start index, you can put the same in the sequence to be assigned. Let's take a look.
The slice assignment removes the item at the start index. Therefore, we need to ensure to keep the element we are replacing in the sequence content.
>>> a = [4, 1]
>>> a[0:1] = [a[0], 3, 2] # Insert multiple items at a given index
[4, 3, 2, 1]
This method allows us to insert multiple items at a given index.
To insert an element at a particular position or index, Python provides a built-in method insert()
. The insert()
method takes two arguments, index
and the object
, and inserts the object
before the index
.
>>> d = [1, 3, 4]
>>> d.insert(1, 2) # Insert `2` before the second item
>>> d
[1, 2, 3, 4]
>>> d.insert(len(d), 5) # Insert `5` at the end
>>> d
[1, 2, 3, 4, 5]
Like the append()
method, the insert()
method inserts only a single item. To insert multiple items, we can assign list items to slice.
The append()
and extend()
method can be replicated using a slice assignment.
>>> a = [1, 2, 3, 4]
>>> a[len(a):] = [5] # Same as a.append(5)
>>> a
[1, 2, 3, 4, 5]
>>> a[len(a):] = [6, 7, 8] # Same as a.extend([6, 7, 8])
[1, 2, 3, 4, 5, 6, 7, 8]
What is the output of the following code?
>>> z = [1, 2, 3]
>>> z[len(z):] = [4, 5, 6]
>>> z
[1, 2, 3, 4, 5, 6]
[1, 2, 4, 5, 6]
[4, 5, 6]
[1, 2, 3, [4, 5, 6]]
Python also provides methods to remove elements from the list. Earlier, we used the del
keyword to remove elements. Let's take a look at other ways to do the same.
Clear, Remove and Pop
To remove all the items of a given list s
, Python provides a built-in method clear()
for list-objects. This is the same as deleting the entire list slice using the del
keyword.
>>> a = [1, 2, 3]
>>> a.clear() # Same as del s[:]
>>> a
[]
To remove a given item without specifying its index, you can use the built-in method remove()
.
The method takes an object
as an argument and removes the first item in the list, which matches the object's value.
>>> a = [1, 2, 1, 2, 3, 4]
>>> a.remove(1) # [2, 1, 2, 3, 4]
>>> a.remove(2) # [1, 2, 3, 4]
>>> a
[1, 2, 3, 4]
The method s.remove(x)
raises ValueError
when x
is not found in the list s
.
Sometimes, you require to return an item and simultaneously remove it from the list. The built-in method pop()
takes the index of the element as an argument. It returns the element while removing it from the list.
>>> a = [1, 2, 3]
>>> a.pop(1) # Return the item at position 1
2
>>> a # Item removed from the list
[1, 3]
How does Python evaluate the last line in the below code?
>>> z = [1, 2, 1, 2, 1, 3, 4, 5, 6]
>>> z.pop()
6
>>> z.pop()
5
>>> z.remove(1)
>>> z.remove(4)
>>> z.remove(6)
- Raises
ValueError
[2, 1, 2, 1, 3]
[2, 1, 2, 1, 3, 6]
[1, 2, 1, 3]
Copy and Replication
Python provides a built-in method copy()
to copy the list. This is the same as the slice s[:]
.
Let's take a look. Let's create a list a
which value [1, 2, 3]
. Now, let's copy the list a
to the name b
.
>>> a = [1, 2, 3]
>>> b = a.copy() # Same as a[:]
>>> b
[1, 2, 3]
To replicate items in a list, we can the augmented assignment operator *=
.
>>> a = [1, 2, 3]
>>> a *= 3 # Same as a = a*3
>>> a
[1, 2, 3, 1, 2, 3, 1, 2, 3]
As we earlier mentioned, both copy and replication produces shallow copy of the items stored in the list.
What's the final value of the a
?
>>> a = [[], [1,2,3]]
>>> b = a.copy() # Create a list `b` with same contents as `a`
>>> b
[[], [1,2,3]]
>>> b[1][1] = 100 # Modify a item stored in `b`
>>> b
[[], [1, 100, 3]] # `b` is modified
>>> a
[[], [1, 2, 3]]
[[], [1, 100, 3]]
Reverse
Suppose you want to complete the reverse the list. This can be achieved using extended slice objects with the step $-1$.
>>> a = [1, 2, 3, 4]
>>> a[::-1] # Slice object with step -1
[4, 3, 2, 1]
>>> a
[1, 2, 3, 4] # The original list is intact
The thing to be noted is that the slice object returns another list.
>>> a = [1, 2, 3, 4]
>>> b = a[::-1]
>>> a is b
False
We can modify the same list object using a python built-in method reverse()
.
>>> a = [1, 2, 3, 4]
>>> a.reverse()
>>> a # The object is modified
[4, 3, 2, 1]
What is the output of the last line of the code below?
>>> a = [1, 2, 3, 4]
>>> b = a[::-1]
>>> a.reverse()
>>> a == b
True
False
Sorting
Earlier, we used the built-in function sorted()
to sort the items in a list. The sorted()
function doesn't change the order of the items in a list; rather, it returns a copy of the list with the items rearranged.
We can change the items of a list object using a built-in python method sort()
. The sort()
method, similar to the sorted()
function, accepts a keyword argument reverse
to get the items in descending order.
>>> a = [3, 5, 2, 1, 4]
>>> a.sort() # Sorts the list in ascending order
>>> a
[1, 2, 3, 4, 5]
>>> a.sort(reverse=True) # Sorts the list in descending order
[5, 4, 3, 2, 1]
Tuples
What's the value of the sorting operation below?
>>> z = [(1, 2), (1, 2, 3), (3, 2, 1), (3, 2)]
>>> z.sort(reverse=True)
[(3, 2, 1), (3, 2), (1, 2, 3), (1, 2)]
[(1, 2), (1, 2, 3), (3, 2), (3, 2, 1)]
[(1, 2), (1, 2, 3), (3, 2, 1), (3, 2)]
[(1, 2, 3), (1, 2), (3, 2), (3, 2, 1)]
To make sense of the previous exercise, you might recall that the tuples are compared lexicographically. Next, let's look into tuples in more detail. However, since tuple
is immutable, there are not many specific operations for a tuple.
Tuples are immutable sequences, and we can create them in several ways:
- Using a pair of parentheses to denote the empty tuple:
()
- Using a trailing comma for a singleton tuple:
a
, or(a,)
- Separating items with commas:
a, b, c
or(a, b, c)
- Using the
tuple()
built-in constructor :tuple()
ortuple(sequence)
To illustrate, let's look at the following code sample.
>>> a = () # Empty Tuple
>>> b = 1, # Singleton tuple same as b = (1, )
>>> c = 1, 2, 3 # Tuple containing values b = (1, 2, 3)
>>> d = tuple([1, 2, 3]) # (1, 2, 3)
>>> e = tuple("Hello") # ('H', 'e', 'l', 'l', 'o')
The tuple(iterable)
constructor builds a tuple
whose items are the same and in the same order as iterable items. Tuple implements all the common sequence operations, as shown earlier.
Python reuses the same empty tuple
object throughout the program execution. Let's take an example to understand more.
>>> a = () # Empty Tuple 1
>>> b = () # Empty Tuple 2
>>> a is b
True # a and b both refer to the same object in memory
Since tuples are immutable, we cannot modify them after they have been created. Therefore, Python reuses the object. But then what happens with the empty mutable containers? Let's take a look.
For empty mutable sequences such as list, Python creates new objects.
>>> x = []
>>> y = []
>>> x is y
False # x and y both refer to the different object in memory
Tuples are immutable, so once created, their value doesn't change. In the next section, let's take a look at another immutable sequence: Strings.
Strings
In chapter 1, we learned about Unicode characters.
Strings are immutable sequences of Unicode code points or characters.
We can create string sequences in several ways:
- String literal using single quotes
''
- String literal using double quotes
""
- Triple quoted string literal
""" """
- String constructor
str()
>>> a = "Can embed 'single quotes' " # Double quotes
>>> b = 'Can embed "double quotes" ' # Single quotes
>>> c= """Can span
multiple lines
of text
""" # Triple quote
>>> d = str(14.3) # Returns the string version of the object
String literals that have whitespace between them will be converted to a single string literal.
>>> e = "Hello" "World" # 'HelloWorld'
Although a string is composed of characters, there is no character type in Python.
A single character is referred to as a string with a length of $1$.
What is the output of the last line of the code below?
>>> a = "ABC"
>>> b = "ABC"
>>> a[0] is b[0]
True
False
Every character has a constant value that is shared among strings. Similar to list
objects, strings have specific methods for them. Let's take a look at them.
String Methods
String sequences implement all of the standard sequence operations and have additional methods of their own. Table 7 shows some of the essential methods categorized by their functionality.
Method Type | Description |
---|---|
Case Modifiers | Modifies the cases of the characters in a string |
Count and Search | Checks the count of characters or search for a substring |
Boolean Methods | Check if any or all digits are specified to a format |
Join and Split | Join or Splits the string using a delimiter |
Strip | Strips the string of a substring |
Format | Formats the string to include a name |
Let's check each of them in a bit more detail. We will start case modifiers.
Case Modifers
A string str
can change the case of some or all of its characters using built-in methods. Table 8 illustrates some of such methods.
Method | Description |
---|---|
str.capitalize() |
Return a copy of the string with its first character capitalized and the rest lowercase. |
str.lower() |
Return a copy of the string with all the cased characters converted to lowercase |
str.swapcase() |
Return a copy of the string with uppercase characters converted to lowercase and vice versa. |
str.title() |
Return a title case version of the string where words start with an uppercase character, and the remaining characters are lowercase |
str.upper() |
Return a copy of the string with all the cased characters converted to uppercase |
We can check the usage of a method in a code listing below.
>>> "hello world".capitalize() # 'Hello world'
>>> "HELLO WORLD".lower() # 'hello world'
>>> "Hello World".swapcase() # 'hELLO wORLD'
>>> "hello world".title() # 'Hello World'
>>> "hello world".upper() # 'HELLO WORLD'
capitalize()
and the title()
method?
The difference between the method capitalize()
and title()
is that the capitalize()
methods change the case of the first letter of the string. In contrast, the title()
method changes every word's first letter in a string.
Count and Search Methods
We can search for the position of a substring or count the substring instances using these built-in Python string methods.
Method | Description |
---|---|
str.count(sub, start, end) |
Returns count of non-overlapping occurrences of substring sub |
str.find(sub, start, end) |
Return the lowest index in the string where substring sub is found |
str.rfind(sub, start, end) |
Return the highest index in the string where substring sub is found |
str.index(sub, start, end) |
Same as find() but raises ValueError when string not found |
str.rindex(sub, start, end) |
Same as rfind() but raises ValueError when string not found |
Earlier, we saw the count()
method in the common operations of sequences. In strings, we can take substrings and count their non-overlapping occurrences. The non-overlapping occurrences mean that the substring doesn't share its characters with earlier found occurrences.
We can also use the optional start and end arguments to define a slice of string to search.
>>> "ananananana".count("ana")
3 # `ana` n `ana` n `ana`
>>> "ananananana".count("ana", 0, 4)
1
What is the value of the following code in Python?
>>> a = "She sells seashells by the sea-shore"
>>> a.count("se")
- 0
- 1
- 2
- 3
Find and Index
The find()
method searches for a string and returns the lowest position where the substring is found. You can also provide an optional start
and end
range to define a slice to search in.
>>> "ananananana".find("ana")
0
>>> "ananananana".find("ana", 4)
4
To get the highest position to find the substring, Python provides another method, str.rfind(sub, start, end)
.
>>> "ananananana".rfind("ana")
8
>>> "ananananana".rfind("ana", 4)
4
When a substring is not found in a given string, the method returns -1
.
>>> "ananananana".find("man")
-1
Unlike find()
and rfind()
, the built-in index()
and rindex()
method return a ValueError
when a substring is not found. Next, we will look into boolean methods associated with strings.
Functions which can return either True
or False
are called Boolean Functions.
String sequences in Python have methods that check if the given string satisfies a particular condition and return True
or False
. These methods are called Boolean Methods.
Boolean Methods
Earlier, we saw the membership operator ( in
) in Python, which checks if a given substring exists in Python. Python strings also have startswith()
and endswith()
, which check if a substring exists in the beginning and end of the string, respectively.
Method | Description |
---|---|
str.endswith(suffix, start, end) |
Check if a suffix present at the end of the string |
str.startswith(prefix, start, end) |
Check if a prefix present at the beginning of the string |
Let's take an example to illustrate both the methods.
>>> a = "Happiness"
>>> "ess" in a
True
>>> a.endswith("ess")
True
>>> a.startswith("Ha")
True
You can use optional start
and end
arguments to select a slice of the string. We can also provide a tuple of suffixes and prefixes. The methods will return True
if one of the items in the tuple satisfies the condition.
>>> b = "Emptiness"
>>> b.endswith(("mess", "chess", "ness", "less"))
True
The other methods relate to the type of characters and their cases used to construct the string. Table 11 lists such methods.
Method | Description |
---|---|
str.isalnum() |
Return True if all characters in the string are alphanumeric and there is at least one character |
str.isalpha() |
Return True if all characters in the string are alphabetic and at least one character. |
str.isascii() |
Return True if the string is empty or all characters in the string are ASCII, False otherwise. |
str.isdecimal() |
Return True if all characters in the string are decimal characters and there is at least one character |
str.isdigit() |
Return True if all characters in the string are digits and there is at least one character. |
str.isidentifier() |
Return True if the string is a valid identifier according to the language definition. |
str.islower() |
Return True if all cased characters in the string are lowercase and at least one cased character. |
str.isnumeric() |
Return True if all characters in the string are numeric characters, and there is at least one character |
str.isspace() |
Return True if there are only whitespace characters in the string and there is at least one character, False otherwise. |
str.istitle() |
Return True if the string is a title-cased string and there is at least one character |
str.isupper() |
Return True if all cased characters in the string are uppercase and at least one cased character. |
What is the output of the following code?
>>> a = " ".isspace()
>>> b = "Hello World".istitle()
>>> c = "Hello World".isalpha()
>>> d = "helloworld".islower()
>>> a, b, c, d
(True, True, True, True)
(True, False, False, True)
(True, False, True, False)
(True, True, False, True)
Next, we will look into the join and split methods in Python.
Join and Split
We can join the string objects stored in a list using the string method str.join()
. The method takes a sequence of string objects and joins them to create a new string object. The separator between the items in the sequence is the string object, which calls this method. For instance,
>>> a = ["This", "is", "ancient", "alien", "tech"]
>>> "-".join(a) # Join the items in a separated by `-`
'This-is-ancient-alien-tech'
We can also split a string into different substrings separated by a delimiter string.
For example,
>>> "Eena,Meena,Deeka".split(",") # Split the string by comma
['Eena', 'Meena', 'Deeka']
The split method accepts a positional argument, maxsplit
, which takes an integer determining how many splits should take place.
# Split once
>>> "Eena, Meena, Deeka, Daai, Daam, Nika".split(",", 1)
['Eena', 'Meena, Deeka, Daai, Daam, Nika']
# Split thrice
>>> "Eena, Meena, Deeka, Daai, Daam, Nika".split(",", 3)
['Eena', 'Meena', 'Deeka', ' Daai, Daam, Nika']
What is the output of the following code?
>>> a = ["Hey", "there", "Delilah", "!"]
>>> b = "-".join(a)
>>> c = b.split("-", 2)
>>> c
['Hey', 'there', 'Delilah-!']
['Hey', 'there-Delilah-!']
['Hey-there-Delilah-!']
['Hey', 'there', 'Delilah', '!']
Next, we will look at stripping strings.
Strip
The str.strip(chars)
method returns a copy of the string object with the leading and trailing characters removed. It takes an optional chars
argument, which specifies the set of characters to be removed. If omitted or None
, the method removes whitespace.
>>> "--| Great Wall of China |--" .strip("-| ") # Notice the space char as well
'Great Wall of China'
The method removes characters from the leading end until reaching a string character that is not present in the set of characters in chars
. A similar action takes place on the trailing end.
If you wish to strip only leading or trailing characters, you can use methods str.lstrip(chars)
or str.rstrip(chars),
respectively.
>>> "--| Great Wall of China |--".lstrip("-| ")
'Great Wall of China |--'
>>> "--| Great Wall of China |--".rstrip("-| ")
'--| Great Wall of China'
What's the output of the below code?
>>> a = "*_* ()_() Brown (o)_(o) Flower ()_() *_*"
>>> a.strip("_()*")
' ()_() Brown (o)_(o) Flower ()_() '
'Brown (o)_(o) Flower'
Brown 0 0 Flower
()_() Brown (o)_(o) Flower ()_()
Formatting
One of the most useful operations on string objects is formatting. To understand this, let's take an example of the printing powers of a given number and some descriptive text.
>>> print("The product of 2 and 5 is", 2*5)
The product of 2 and 5 is 10
For some other numbers, we can write.
>>> print("The product of 4 and 8 is", 4*8)
The product of 4 and 8 is 32
You might notice that while we are repeating the text and changing the number. This is a case where the formatting string is effective.
>>> text = "The product of {} and {} is {}"
>>> text.format(2, 5, 2*5)
'The product of 2 and 5 is 10'
>>> text.format(4, 8, 4*8)
'The product of 4 and 8 is 32'
Formatting lets you define some replacement fields inside the string literal which you can fill up values from objects later on.
What's the output of the below code?
>>> text = "The {} {} {} jumps over the {} {}"
>>> text.format("quick", "brown","fox","lazy","dog")
'The quick brown fox jumps over the lazy dog'
'The brown quick fox jumps over the lazy dog'
'The fox brown quick jumps over the lazy dog'
'The quick brown dog jumps over the lazy fox'
String Formatting
String formatting is an important part of programming in Python. Python provides several ways to format a string.
We will start with formating simple string literals.
>>> text = "{} apple a {}, keeps the {} away"
>>> text.format('An', 'day', 'doctor')
'An apple a day, keeps the doctor away'
The {}
closed curly braces define the replacement fields or placeholders, which expect value from the argument tuple provided to the format()
method. The position of the arguments corresponds to the position of the replacement fields. The first argument goes to the first replacement field, the second goes to the second, so on and so forth.
If the length of the argument tuple to the format()
method is lesser than the replacement fields, Python will raise IndexError
.
>>> text.format('An', 'apple')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
Positional Placeholders
When the position of the arguments corresponds to the replacement fields' position, the replacement fields are said to be positional placeholders.
You can define the replacement fields with a position to specify which item from the argument tuple is to be taken.
>>> text = "{3} you {0}, {2} shall you {1} !"
>>> text.format("sow", "reap", "so", "As")
'As you sow, so shall you reap !'
If there is a mismatch of index specified and the argument tuple, Python will raise an error. If you wish to reuse the same variable again, you can do it by specifying positional placeholders.
>>> text = "O {0}! my {0}! our fearful trip is done."
>>> text.format("Captain")
'O Captain! my Captain! our fearful trip is done.'
named placeholder
. Do you want to guess as to what a named placeholder?
Named Placeholders
Placeholder with keys or names is called named placeholders.
The str.format()
method also accepts keyword arguments. We can specify the placeholders with keys and provide the format()
method named arguments against those keys.
>>> text= "My best friends are {fr1} and {fr2}"
>>> text.format(fr1="Dinesh", fr2="Gilfoyle")
'My best friends are Dinesh and Gilfoyle'
We can use a mix of named and positional placeholders. Just keep in mind while providing method arguments that positional argument cannot follow keyword arguments.
>>> text = """We are here to {}
at the odds
and live our lives
so well that {}
will {} to take us.
{first} {last}
"""
>>> formatted_text = text.format("laugh", "Death", "tremble",
first="Charles",
last="Bukowski")
>>> print(formatted_text)
We are here to laugh
at the odds
and live our lives
so well that Death
will tremble to take us.
Charles Bukowski
What's the output of the following code?
>>> text = "The {0} brown {2} {verb} {1} the lazy {noun}"
>>> text.format("quick","over", "fox", verb="jumps", noun="dog")
'The quick brown fox jumps over the lazy dog'
'The quick brown over jumps fox the lazy dog'
'The quick brown fox dog over the lazy jumps'
- Raises
IndexError
Number Formatting
You can format the numbers using a set of placeholder formats in a string literal. We can define the precision of floating points in the string literal using the following syntax:
$$
:(width). (precision)f
$$
Here the width specifies the number of places the decimal should occupy. If the width exceeds the number of digits, the position is filled with space. The precision specifies the number of significant digits to display in the string literal. For instance,
>>> "{}".format(22/7)
'3.142857142857143'
>>> "{:5.1f}".format(22/7) # Occupy 5 places and 1 decimal digit
' 3.1'
>>> "{:5.3f}".format(22/7) # Occupy 5 places with 3 decimal digit
'3.143'
If the length of the precision is greater than the width, then the precision becoms the width.
>>> "{:1.7f}".format(22/7) # Occupy 1 place with 7 decimal digits
'3.1428571' # Occupies 9 places instead of 2 digit
The integers can also be formatted to display a number with a comma separator.
>>> "{:,}".format(1e9)
'1,000,000,000.0' # Comma Separated Value
Format values to different bases
A number can be formatted to be represented in different number bases such as decimal (d
), hexadecimal (x
), octal ( o
), or binary ( b
).
>>> print("Decimal:{0:d} Hex:{0:x} Octal:{0:o} Binary:{0:b} ".format(124))
Decimal:124 Hex:7c Octal:174 Binary:1111100
We can see additional details about string formatting on the Python documentation page.
What's the output of the following code?
>>> from math import pi
>>> "Value of pi: {2:5f}".format(pi)
- 'Value of pi: 3.14159'
- 'Value of pi: 3.1415'
- 'Value of pi: 3.141'
- 'Value of pi: 3.142'
There is another way to format strings, which is much more convenient to use: f-strings.
F-Strings
A formatted string literal or f-string is a string literal that is prefixed with $f$ or $F$.
F-strings was introduced from Python 3.6 to make string formatting easier. The f-strings tend to lot less verbose than using the str.format()
method. Let's see a couple of examples to understand more.
>>> first_name = "Monkey"
>>> middle_name = "D."
>>> last_name = "Luffy"
>>> f"Hello, {first_name} {middle_name} {last_name} !"
'Hello Monkey D. Luffy !'
We can also use expressions inside the replacement fields in f-string.
>>> f"{45*5}" # Can evaluate expressions directly
'225'
And you can also call functions or methods inside the replacement fields in an f-string.
>>> name = "MONKEY D. LUFFY"
>>> f"Hello, {name.title()} !" # Can call method directly
'Hello, Monkey D. Luffy !'
You can also write multi-line f-strings.
>>> name, age, dream = "Monkey D. Luffy", "16", "Pirate King"
>>> text = (
... f"Hi, I am {name}! "
... f" I am {age} years old."
... f" I will be the {dream}."
... )
>>> text
'Hi, I am Monkey D. Luffy! I am 16 years old. I will be the Pirate King. '
You have to prefix each line with f to write multi-line strings using double or single-quoted strings.
You can also use triple-quoted f-strings.
>>> text = f"""
... Hi, I am {name}!
... I am {age} years old.
... I will be the {dream}.
..." ""
>>> print(text)
... Hi, I am Monkey D. Luffy!
... I am 16 years old.
... I will be the Pirate King.
What's the output of the below code?
>>> pirate = {
'name': 'Luffy',
'age': 16
}
>>> f'The pirate named {pirate['name']} is {pirate['age']} years old. '
'The pirate named Luffy is 16 years old. '
'The pirate named Luffy is {pirate['age']} years old. '
'The pirate named {pirate['name']} is {pirate['age']} years old. '
- Raises
SyntaxError
You should avoid using the same type of quotations around the dictionary keys as you do with f-strings.
This brings us to the end of the section of strings and sequences. Let's look at sets in the next section.
Set Types
You might have read about sets and set theory at some point while learning mathematics. The basics of set theory are quite simple and can be easily understood by everyone.
In the real world, a set is a group or collection of things that belong together or resemble one another or are usually found together—for example, A set of teeth.
Mathematically, it's the definition is more precise.
A set is a collection of distinct objects. These distinct objects are referred to as members of the set.
Previously we saw sequences are ordered collections of objects. A set is an unordered collection of unique objects. Unlike sequences, sets do not provide any indexing or slicing operations. The elements of the set must be of an immutable type.
We can create Python sets in several ways,
- Built-in set constructor which takes an iterable,
set(<iter>)
- Using curly braces,
{1,2,3}
For instance,
>>> a = set([1, 2, 3, 2, 4, 5 ,6, 6 ,1 ,3 ,5]) # List with duplicate items
>>> a
{1, 2, 3, 4, 5, 6} # Set with distinct items
When you construct a set from an iterable, Python removes duplicate items and creates a set of unique items from the given list. This is also applicable to string sequences.
# Distinct characters in the string
>>> b = set("mississippi missouri")
{'o', 'i', 'u', 'r', 'm', 'p', ' ', 's'}
You can notice that the strings' resulting set is unordered, and the original order specified by the sequence is not necessarily preserved.
What is the output of the below code?
>>> c = {1, 2, 3, 2, 4, 5 ,6, 6 ,1 ,3 ,5}
>>> c
{1, 2, 3, 4, 5, 6}
{1, 2, 3, 2, 4, 5, 6, 6, 1, 3, 5}
{}
{1, 2, 3, 2, 4, 5, 6}
The code listing in the above exercise demonstrates the construction of sets using curly braces.
When you define a set using curly braces {}
, Python automatically removes the duplicate objects. However, unlike the set()
constructor, the iterable is not broken down to their constituent objects while being defined using curly braces {}
. The objects inside curly braces {}
are placed into the set intact, even if they are iterable.
>>> set("Hello") # Creating sets using built-in set() func
{'e', 'o', 'l', 'H'}
>>> {'Hello'} # Creating sets using curly braces
{'Hello'}
A set can be empty. However, you cannot define an empty set using curly braces {}
{}
?
The reason is Python interprets empty curly braces as empty dictionary objects.
>>> a = {}
>>> type(a)
<class 'dict'>
Therefore, the only way to define an empty set is by using the set()
constructor.
>>> b = set()
>>> type(b)
<class 'set'> # Set
>>> b
set() # Empty Set
The truth value of an empty set is False
.
>>> c = set()
>>> c or 1
1
>>> c and 1
set()
To verify an empty set's truth value, we can use the bool()
function.
>>> bool(set()) # Truth Value
False
As we mentioned above, the set elements must be immutable. Mutable containers such as Lists and Dictionaries are not allowed in a set. At the same time, tuples are allowed in sets.
So, the below code is a perfectly acceptable set.
>>> a = {(1,2,3),"Hello", 1, 1}
>>> a
{1, 'Hello', (1, 2, 3)}
While the below set is unacceptable as it includes a mutable dictionary.
>>> b = {{}, 1, 2 }
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
Similarly, Python raises an error for the below set as it includes a list
object.
>>> c = {[1, 2, 3], 1, 2}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
What's the result of the following code?
>>> set([1, 2, 3, 4, (1, 2, 3)])
{1, 2, 3, 4, (1, 2, 3)}
{1, 2, 3, 4, [1, 2, 3]}
Raises TypeError
{1, 2, 3, 4}
Membership and Copy Operation on Sets
Sets in Python also support several built-in functions and operations. Let's start with getting the length of a set.
Like sequences, we can determine the number of members in a set by using the built-in len()
function.
>>> x = {'a', 'b', 'c'}
>>> len(x)
3
To check if a member exists in a set, the operators in
and not in
can be used.
>>> 'a' in x
True
>>> 'c' not in x
False
You can also create a copy of a set using a built-in method s.copy()
, which returns a shallow copy of the set items.
>>> s1 = {1, 2, 3}
>>> s2 = s.copy()
>>> s2
{1, 2, 3}
What's the output of the below code?
>>> s1 = set("Hello World")
>>> " " in s1, len(s1)
(True, 8)
(True, 7)
(False, 7)
(False, 8)
Mathematical Operations on Sets
We can implement certain mathematical operations on sets of its built-in method. The following mathematical operations are usually performed on sets:
- Union
- Difference
- Intersection
- Symmetric Difference
- Disjoint
- Superset
- Subset
We will understand these operations along with the description of each operation. Let's start with Union operation.
Union
For two sets $s1$, $s2$, a set of all objects that a member of $s1$or $s2$ or both is referred to as union of sets.
In Python, the union of two or more sets can be computed using the built-in union operator |
or built-in union()
method.
>>> s1, s2, s3 = {1, 2, 3}, {4, 5}, {6, 7}
>>> s1 | s2 | s3 # Union of sets s1, s2 and s3 using operator
{1, 2, 3, 4, 5, 6, 7}
>>> s1.union(s2, s3) # Union using method
{1, 2, 3, 4, 5, 6, 7}
The resulting set comprises all elements present in any of the specified sets.
What's the output of the code below?
>>> len(set("Hello") | set("World"))
- 7
- 8
- 9
- 6
Difference
The set difference of $s1$ and $s2$ is the set of all members of $s1$ that are not members of $s2$.
In Python, the difference of two sets can be obtained either using operator -
or built-in set method difference()
.
>>> s1, s2 = {1, 2, 3}, {1, 3}
>>> s1 - s2 # Difference using operator
{2}
>>> s1.difference(s2) # Difference using method
{2}
The resulting set from the $s1 - s2$ operation returns the set containing members present in $s1$ but not $s2$.
When there are multiple sets defined, the operation is performed from left to right.
>>> s1, s2, s3, s4 = {1, 2, 5, 7}, {1, 3}, {2, 3}, {1, 5}
>>> s1 - s2 - s3 - s4 # Difference using operator
{7}
>>> s1.difference(s2, s3, s4) # Difference using method
{7}
In the above code listing, the operation $s1 - s2$ is performed first, which results in ${2, 5, 7}$. Then $s3$ is subtracted from the resulting set to obtain ${5, 7}$. Finally $s4$ is subtracted from ${5, 7}$ to get ${7}$.
What's the output of the code below?
>>> text1 = "the five boxing wizards jump quickly"
>>> text2 = "the quick brown fox jumps over the lazy dog"
>>> len(set(text1) - set(text2))
- 0
- 10
- 13
- 5
In the previous exercise, both the sentences are pangrams[^5]. Now, let's take a look at intersection operation on sets.
Intersection
Intersection of the sets $s1$ and $s2$ is the set of all objects that are members of both $s1$ and $s2$.
In Python, the intersection of two or more sets can be obtained using the operator &
or the built-in set method intersection()
.
>>> s1, s2, s3 = {1, 2, 3, 5}, {3, 5}, {1, 3, 5, 7}
>>> s1 & s2 & s3 # Intersection using operator
{3, 5}
>>> s1.intersection(s2, s3) # Intersection using method
{3, 5}
What's the output of the code below?
>>> s1 = set([1, 2, 3]).intersection(set([1, 4, 5]), set([2, 3, 6]))
set()
{3}
{3, 1}
{1, 2, 3}
Symmetric Difference-
The symmetric difference of sets $s1$ and $s2$ is the set of all objects that are a member of precisely one of $s1$ and $s2$ and not in both sets.
In Python, the symmetric difference of two or more sets can be determined using the operator ^
or the symmetric_difference()
method.
>>> s1, s2 = {1, 2, 3}, {4, 5, 2, 3 }
>>> s1 ^ s2 # Symmetric Difference using operator
{1, 4, 5}
>>> s1.symmetric_difference(s2)
{1, 4, 5} # Symmetric Difference using method
What's the output of the code below?
>>> s1, s2 = {1, 2, 3, 6}, {7, 3, 2, 1}
>>> s1 ^ s2
{1, 2, 3}
{6, 7}
set()
{1, 2, 3}
Disjoint
Two sets s1 and s2, are said to be disjoint sets if they have no common element.
As disjoint sets have no common members, the intersection results in an empty set.
We can also state that two sets are said to disjoint if the intersection results in an empty set.
Python provides a built-in method for set type objects, isdisjoint()
, to check two or more sets are disjoint.
>>> s1, s2 = {"Apples", "Berries"}, {"Grapes", "Peach"}
>>> s1.isdisjoint(s2) # Check if sets are disjoint
True
>>> s1 & s2 # Intersection should be empty
set()
No operator corresponds to the built-in set method isdisjoint()
.
What's the output of the code below?
>>> s1, s2 = set([1, 2, 3]), set([2,3, 4])
>>> s3 = s1 - s2
>>> s3.isdisjoint(s1)
True
False
Subset
A set $s1$ is considered a subset of another set $s2$ if every object of $s1$ is a member of $s2$.
In Python, you can check if a set $s1$ is a subset of another set $s2$, using either the built-in set method s1.issubset(s2)
or using the operation $s1 <= s2$.
>>> s1 , s2 = {1, 2, 3}, {4, 1, 2, 3, 5}
>>> s1 <= s2 # Checking subset using operator
True
>>> s1.issubset(s2) # Checing subset using method
True
A set $s1$ can be considered a subset of itself because it contains every member of itself. We can check this in Python as well.
>>> s1 = {1, 2, 3}
>>> s1 <= s1 # s1 can be subset of itself
True
A proper subset of a set $s1$ is defined as a subset that is not identical to the set $s1$.
So, a set $s1$ is considered a proper subset of $s2$ if every element of $s1$ is a member of $s2$, and $s1$ and $s2$ are not equal. We can check if a set is a proper subset of another using the operator <
.
>>> s1, s2 = {1, 2, 3}, {1, 2, 3, 4}
>>> s1 < s2
True
What's the output of the code below?
>>> s1 = {1, 2, 3}
>>> s1 < s1
True
False
A set cannot be a proper subset of itself. Next, we will take a look at a superset.
Superset
A set $s1$ is considered a superset of another set $s2$ if $s1$ contains every element of $s2$.
A superset is the reverse of a subset. In Python, we can check if a set $s1$ is a superset of a set $s2$ using the operator >=
or the built-in set object method s1.superset(s2)
. A set is also considered a superset of itself.
>>> s1, s2 = {1, 2, 3, 4}, {1, 2, 3}
>>> s1 >= s2 # Checking superset using operator
True
>>> s1.superset(s2) # Checking superset using method
True
>>> s1 >= s1 # s1 can be a superset of itself
True
A proper superset is the same as a superset except that the two sets cannot be identical. In Python, we can check if a set is a superset of another set using the operator >.
>>> s1, s2 = {1, 2, 3, 4}, {1, 2, 3}
>>> s1 > s2 # Checking proper superset
True
>>> s1 > s1 # s1 can not be a superset of itself
False
What's the output of the code below?
>>> s1 = {1, 2, 3}
>>> s1 > s1
True
False
Once again, a set cannot be a proper superset of itself.
Next, we will look at the ways how to modify a set.
Modifying Sets
Even though sets only contain immutable objects, sets themselves can be modified to include or remove items or members. We can use the operations we listed in the earlier sections to update an existing set using augmented assignment operators. Table 12 shows set update methods.
Operation Name | Method | Augmented Operator |
---|---|---|
Union Update | s1.update(s2) |
|= |
Intersection Update | s1.intersection_update(s2) |
&= |
Difference Update | s1.difference_update(s2) |
-= |
Symmetric Difference Update | s1.symmetric_difference(s2) |
^= |
The augmented operator, as you might recall, is the short-hand form of the expanded form. This is shown in the table 13.
Augmented Operation | Expanded Operation |
---|---|
s1 |= s2 | s3 |
s1 = s1 | s2 | s3 |
s1 &= s2 & s3 |
s1 = s1 & s2 & s3 |
s1 -= s2 - s3 |
s1 = s1 - s2 - s3 |
s1 ^= s2 ^ s3 |
s1 = s1 ^ s2 ^ s3 |
Let's start with the Union Update Method.
Union Update Method
Let's say you wish to update a set $s1$ with its union with other sets $s2$ and $s3$. We can do that using the assignment operator.
>>> s1, s2 = {1, 2} ,{3, 4, 5, 6}
>>> s1 = s1 | s2 # Update s1 to union of sets of s1 and s2
>>> s1
{1, 2, 3, 4, 5, 6}
Python provides an augmented operator |=
to make this assignment.
>>> s1, s2 = {1, 2} ,{3, 4, 5, 6}
>>> s1 |= s2 # Update s1 using augment assignment
>>> s1
{1, 2, 3, 4, 5, 6}
Python also provides a built-in set object method, update()
to update a set to a set's union.
>>> s1, s2 = {1, 2} ,{3, 4, 5, 6}
>>> s1.update(s2) # Update s1 using method
>>> s1
{1, 2, 3, 4, 5, 6}
You can include multiple sets, and it will take to update the set to the union of all provided sets.
>>> s1, s2, s3, s4 = {1}, {2}, {3, 4}, {5, 6}
>>> s1.update(s2, s3, s4) # Same as s1 |= s2 | s3 | s4
>>> s1
{1, 2, 3, 4, 5, 6}
What's the output of the below code?
>>> s1, s2, s3 = set("aligned"), set("dealing"), set("leading")
>>> s1 |= s2 | s3
>>> s1 == s2, s1 == s3
(True, True)
(False, True)
(True, False)
(False, False)
In the previous exercise, the three words aligned, dealing, leading
are anagrams of each other. Therefore, even after updating the set $s1$ with union with $s2$ and $s3$, it leads to no change in the set.
Next, let's check out the intersection update method.
Intersection Update Method
A set $s1$ can be updated with intersections with other sets using augmented assignment &=
or built-in set object method intersection_update()
. The intersection update s1.intersection_update(s2)
or s1 &= s2
updates $s1$, resulting in $s1$ having objects found in both $s1$ and $s2$. Let's take an example.
>>> s1, s2 = {1, 2, 3}, {2, 3, 4}
>>> s1.intersection_update(s2) # Same as s1 &= s2
>>> s1
{2, 3}
What's the output of the following code?
>>> s1, s2, s3 = set("aligned"), set("dealing"), set("leading")
>>> s1 &= s2 & s3
>>> s1 == s2, s1 == s3
(True, True)
(False, True)
(True, False)
(False, False)
The previous exercise' answer shouldn't be surprising if you recall that the three words are anagrams.
Next, we will take a look at the difference update method.
Difference Update Method
A set $s1$ can be updated with the differences with other sets using augmented assignment operator -=
or built-in set object method difference_update()
. The difference update s1.difference_update(s2)
or s1 -= s2
results in removing objects from $s1$ which are found in $s2$ as well.
>>> s1, s2 = {1, 2, 3}, {2, 3, 4}
>>> s1.difference_update(s2) # Same as s1 -= s2
>>> s1
{1}
What's the output of the code below?
>>> s1, s2, s3 = set("aligned"), set("dealing"), set("leading")
>>> s1.difference_update(s2, s3)
>>> s1 == s2, s1 == s3, s2 == s3
(True, True, True)
(False, True, True)
(True, False, True)
(False, False, True)
Once again, as the three words are anagrams, their difference update results in an empty set.
Next, we will look at the symmetric difference update method.
Symmetric Difference Update Method
A set $s1$ can be updated with symmetric differences with other sets using augment assignment operator ^=
or built-in set object method symmetric_difference()
.
The symmetric difference update s1.symmetric_difference(s2)
or s1 ^= s2
results in a set s1
updated to a set with objects either in s1
or s2
but not both.
>>> s1, s2 = {1, 2, 3}, {2, 3, 4}
>>> s1.symmetric_difference(s2) # Same as s1 ^= s2
>>> s1
{1, 4}
What's the output of the code below?
>>> s1, s2, s3 = {1, 2, 3}, {2, 3, 4}, {3, 4, 5}
>>> s1 ^= s2 ^ s3
>>> s1
set()
{2}
{3}
{4}
As the 3
is the only element common in all three sets, the set s1
gets updated to {3}
. Apart from the above methods, the set objects have additional methods. Let's take a look at them.
Additional Methods for Set
Table 14 below lists some additional methods available on a set object.
Method | Description |
---|---|
s.add(<obj>) |
Adds an immutable to a set |
s.remove(<obj>) |
Remove the object from the set. Raises KeyError if element not found in the list |
s.discard(<obj>) |
Same as remove method but no Exception is raised when object not found |
s.pop() |
Randomly returns an object from a set and removes it |
s.clear() |
Removes all objects from the set |
Adding Items to a set
The method s.add() adds an immutable object to a set.
>>> s = {1, 2, 3, 4}
>>> s.add(5)
>>> s
{1, 2, 3, 4, 5}
Removing Items from a set
The s.remove(obj)
method removes the object from the set and raises KeyError
if the object is not present inside the set.
>>> s = {1, 2, 3, 4, 5, 6}
>>> s.remove(2) # {1, 3, 4, 5, 6}
>>> s.remove(56) # Object missing, will raise Keyerror
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 56
The s.discard(obj)
method works the same as s.remove(obj)
, but it doesn't raise any KeyError
when the object is not found in the set.
>>> s.discard(56) # Object missing, will not raise error
Like list objects, Set objects also have a pop()
method, which retrieves an object from the set while simultaneously removing it from the set.
>>> s.pop() # {2, 4, 5}
1 # Might be different for you.
Removing all elements from a set
Python provides the s.clear()
method for setting objects to remove all the elements in a set.
>>> s.clear()
>>> s
set()
What's the most likely final value of the set s
?
>>> s = set("zebra")
>>> s.discard("z")
>>> s.pop()
'e'
>>> s.add("t")
>>> s.add("r"); s.clear();
>>> s
set()
{'b', 'a', 'r', 't'}
{'b', 'a', 'r', "e"}
{'b', 'a', 'r'}
Frozen Sets
The sets in Python are mutable objects. Python also provides another built-in type called frozenset, which is pretty much similar to set object except a frozenset is immutable. We can define a frozenset using the frozenset(<iter>)
constructor.
>>> f = frozenset([1, 2, 3, 4])
>>> f
frozenset({1, 2, 3, 4})
All non-mutating operations for a set are also applicable to a frozenset.
>>> f.add(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>>> f.remove(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'remove'
The set object methods s.add()
, s.remove(<obj>)
, s.discard(<obj>)
, s.pop()
and s.clear()
are not present for frozenset objects.
Also the update methods of set such s.update()
, s.intersection_update()
, s.difference_update()
and s.symmetric_update()
are not available for frozenset objects as frozensets are immutable.
>>> f1, f2 = frozenset("hello"), frozenset("world")
>>> f1.update(f2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'update'
What's the output of the following?
>>> s1, s2 = frozenset("hello"), frozenset("world")
>>> s1 |= s2
>>> s1
frozenset({'e', 'o', 'h', 'l', 'w', 'r', 'd'})
frozenset({'e', 'o', 'h', 'l', 'w', 'r', 'd'})
set({'e', 'o', 'h', 'l', 'w'})
frozenset({'e', 'o', 'h', 'l', 'w'})
s.update()
, s.intersection_update()
etc) are not available on frozenset
. Although the corresponding augmented operators work fine. Can you guess why this so?
The augmented update operators return a new object and reassign the set name to the newly returned set objects. Let me elaborate.
Let's take a look at what happens when we use the augmented update operator on frozen sets.
>>> f1 = f2 = frozenset([1, 2, 3]) # Two names for the same object
>>> f3 = frozenset([2, 4, 5])
>>> f2 |= f3 # Union Update
>>> f2
frozenset({1, 2, 3, 4, 5}) # f2 gets reassigned
>>> f1
frozenset({1, 2, 3}) # Original object is intact
In the above code sample, the object referenced by f2
didn't get updated or modified; rather, the name f2
got reassigned to the union of the sets f2
and f3
.
As sets are mutable, they cannot be used as keys for a dictionary. Frozensets, being immutable, can be used as keys for a dictionary.
It is impossible to create sets of sets as sets are themselves mutable.
In this case, frozensets can be used to create set objects.
For instance,
>>> f1, f2, f3 = frozenset([1, 2]), frozenset([4]), frozenset([3])
>>> s = {f1, f2, f3}
>>> s
{frozenset({3}), frozenset({4}), frozenset({1, 2})}
Comparisons
Both set and frozenset support set to set comparisons. We can compare sets in the following way:
- Two sets are equal if every element of each set is contained in the other, which means each is a subset of the other.
- A set is less than another set if and only if the first set is a proper subset of the second set.
- A set is greater than another set if and only if the first set is a proper superset of the second set.
What's the output of the following code?
>>> s1, s2, s3 = set([1, 2, 3]), set([1, 2]), set([1])
>>> s = [s1, s2, s3]
>>> sorted(s)
[{1}, {1, 2}, {1, 2, 3}]
[{1, 2, 3}, {1, 2}, {1}]
[{1, 2, 3}, {1}, {1, 2}]
[{1, 2}, {1}, {1, 2, 3}]
With this, we have come to the end of sets in Python. In the next section, we will cover dictionaries.
Dictionary
In this section, we will take a look at the dictionary object.
Dictionaries are used for storing objects against custom keys.
A dictionary is a standard mapping object in Python used for storing values against keys.
Python provides one standard mapping object, the dictionary. We can create a dictionary in several ways:
Comma-separated list of key: value
pairs within braces
>>> d1 = {'a': 1, 'b' : 2, 'c' : 3} # Using curly braces
Using dict()
constructor
The built-in dict()
constructor take one of the following form for arguments:
- Keyword arguments,
a = 1
,b = 2
>>> d2 = dict(a = 1, b = 2, c = 3) # Using keyword arguments
2. Mapping object, {'a':1, 'b':1}
>>> d3 = dict({ 'a': 1, 'b' : 2, 'c' : 3 }) # Using Mapping
3. Iterable object with each item also being iterable with length two, [(a, 1), (b,2)]
>>> d4 = dict([('a', 1), ('b', 2), ('c', 3)]) # Using Iterable
We can verify that all four dictionary objects created above have the same value.
>>> d1
{'a': 1, 'b': 2, 'c': 3}
>>> d1 == d2 == d3 == d4
True
Creating a dictionary using a dict()
constructor with keyword arguments requires that the keys be valid python identifiers.
>>> x = dict(for=1) # Will raise error as `for` is a reserved name
File "<stdin>", line 1
x = dict(for=1)
^
SyntaxError: invalid syntax
>>> x = {"for" : 1} # No error
This limitation is not imposed on other methods of creating a dictionary.
>>> x = {"for" : 1} # No error
What's the result of the below code?
>>> d = dict([(1, Harry), (2, Luffy), (3, Ted)])
- Raises
NameError
{1: 'Harry', 2: 'Luffy', 3: 'Ted'}
{3: 'Harry', 2: 'Luffy', 1: 'Ted'}
{1: 'Harry', 3: 'Luffy', 2: 'Ted'}
For the previous exercise, keep in mind that the string characters must be wrapped in quotes.
We have now learned how to create dictionary objects. Let's now look at how access values in a dictionary object.
Accessing Dictionary Values
Let's create a dictionary object with some countries along with their capitals.
>>> capitals = {
"Afganistan" : "Kabul",
"Bhutan" : "Thimpu",
"Canada" : "Ottawa",
"Denmark" : "Copenhagen",
"Estonia": "Tallinn"
}
We can verify that this is indeed a dictionary.
>>> capitals
{'Afganistan': 'Kabul', 'Bhutan': 'Thimpu', 'Canada': 'Ottawa', 'Denmark': 'Copenhagen', 'Estonia': 'Tallinn'}
>>> type(capitals)
<class 'dict'>
We can retrieve objects stored in sequences such as lists and tuples using the index position.
In contrast, we retrieve objects stored in dictionaries by the key against which the object is stored. We have stored the capitals against the country names. Therefore, the capital are values while the country names are keys against which values are stored.
We will use the country name as the key in a square bracket notation to access individual countries' capitals.
>>> capitals["Bhutan"] # Get the value associated with Bhutan
'Thimpu'
Note that we mentioned earlier that sequences contain ordered elements, while elements in sets and dictionaries are unordered. From Python 3.7, Python guarantees the dictionaries item to be stored in the insertion order.
What's the value of the code below?
>>> capitals
{'Afganistan': 'Kabul', 'Bhutan': 'Thimpu', 'Canada': 'Ottawa', 'Denmark': 'Copenhagen', 'Estonia': 'Tallinn'}
>>> capitals[0]
Kabul
Thimpu
Afganistan
- Raises
KeyError
In the above exercise, since there are no keys with the value 0
, Python raises KeyError
.
In the code listing in the previous exercise, the keys are stored as strings. We can use other immutable objects such as tuples, frozen sets, or numbers, etc. in Python, to be used as a dictionary key to store objects in a dictionary. The keys of a dictionary need not be of the same type.
>>> a, b = (0, 1) , frozenset([0, 1])
>>> c = {
1: "Number",
a: "Tuple",
b: "Frozen Set"
}
>>> c[1] # Key type : Number
'Number'
>>> c[a] # Key type : Tuple
'Tuple'
>>> c[b] # Key type : Frozenset
'Frozen Set'
We can store tuples and frozen sets as dictionary keys.
What's the output of the code below?
>>> a = (1, 2, 0)
>>> d = {0: "Zero", 1: "One", 2: "Two", a: "Tuple"}
>>> d[a[0]]
'Zero'
'One'
- Raises
KeyError
'Tuple'
The key a[0]
first evaluates to $1$, then the expression becomes d[1]
, which is One
. We can also add new key: value
pairs or modify existing values in a dictionary. Let's take a look at how.
Adding and updating dictionary values
Earlier, we created a dictionary object named capitals
. We can add more key: value
pairs to the already created object by using the assignment operator.
>>> capitals["Fiji"] = "Suva"
>>> capitals["Germany"] = "Berlin"
>>> capitals # Dictionary object updated
{'Afganistan': 'Kabul', 'Bhutan': 'Thimpu', 'Canada': 'Ottawa', 'Denmark': 'Copenhagen', 'Estonia': 'Tallinn', 'Fiji': 'Suva', 'Germany': 'Berlin'}
You can also create an empty dictionary and add values to it afterward. You can add dictionaries or lists or tuples as objects too.
>>> person = {}
>>> person["name"] = "John Doe"
>>> person["friends"] = ["Eena", "Meena", "Deeka"]
>>> person["food"] = {"Beverage" : "Lemonade", "Indian" : "Dosa"}
>>> person
{'name': 'John Doe', 'friends': ['Eena', 'Meena', 'Deeka'], 'food': {'Beverage': 'Lemonade', 'Indian': 'Dosa'}}
To access a sublist or subdictionary, an additional index or key is required.
>>> person["food"]["Indian"]
'Dosa'
>>> person["friends"][2]
'Deeka'
If you try to assign to a key that is already present in the dictionary keys, Python overwrites the previous value stored.
>>> person = { "name": "Jane Doe"}
>>> person
{'name': 'Jane Doe'}
>>> person["name"] = "James Doe" # key:value pair is updated
>>> person
{'name': 'James Doe'}
There cannot be two identical keys in a dictionary.
What's the output of the code below?
>>> person = {'name': 'Luffy' }
>>> person["age"] = 16
>>> person = {'ship': 'Thousand Sunny'}
>>> person
{'ship': 'Thousand Sunny'}
{'name': 'Luffy'}
{'name': 'Luffy', 'age': 16}
{'age': 16}
Earlier, we saw that immutable objects could be keys to a dictionary. The limitation imposed by Python is that only those hashable can be used as dictionary keys.
An object is hashable if it has a hash value, which never changes during its lifetime and can be compared to other objects.
To check the hash value of an object, we can use Python's built-in hash()
function. The hash()
function returns the hash-value of a hashable object and raises TypeError
when an object is unhashable.
For instance, for an immutable tuple:
>>> a = (1, 2)
>>> hash(a)
3713081631934410656 # Different for you
While for instance for the mutable list,
>>> a = [] # Mutable Container
>>> hash(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
This is the same TypeError
when you try to use a list as a dictionary key.
>>> person, a = {}, []
>>> person[a] = "Something"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
All the built-in immutable objects we have covered so far are hashable. Immutable containers such as tuples and frozensets are hashable, only if their elements are hashable.
So, for now, you can assume that immutable objects can be used as dictionary keys while mutable objects cannot.
Unlike the limitation of using only hashable and non-identical objects for keys, no such restrictions are imposed on the values stored in a dictionary. You can put any objects as the value inside a dictionary corresponding to a key.
In the below code, what's the value of __A__ for which Python prints Nami?
>>> a, b, c, d = [0, 1, 2, 3]
>>> d = {a: "Luffy", b: "Zorro", c: "Sanji", d: "Nami"}
>>> __A__ # What's the expression?
'Nami'
d[a]
d[d]
d[3]
d[2]
Built-in Methods of Dictionary
Similar to sequences, dictionary objects also contain some built-in methods which are applicable only for dictionary objects. Let's take a look at them in detail.
Retrieving Dictionary Items
Earlier, we saw that we could access or retrieve a dictionary item using the key of the value stored in square brackets.
>>> person = {"name" : "John Doe", "age" : 26}
>>> person["name"]
'John Doe'
If we use any key for which no value has been stored in the dictionary, Python raises an error.
>>> person['weight']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'weight'
The dictionary objects also expose a method d.get(<key>, <default>)
, which retrieves the item us and fails silently if the key doesn't exist in the dictionary.
>>> person.get("name") # Retrieving item using get method
'John Doe'
>>> person.get("weight") # Fails silently
>>>
The built-in method d.get(k, d)
accepts an optional argument that can return a default value if no key is found.
>>> person.get("weight", "No record of weight available for the person.")
'No record of weight available for the person.'
What's the output of the below code?
>>> pirate = dict(name="Luffy", age="15")
>>> pirate["Ship"] = "Going Merry"
>>> pirate.get("ship", "Sorry")
'Sorry'
'Going Merry'
Raises KeyError
Python has additional built-in methods to retrieves the entire list of keys, values, or both of a given dictionary. Table 15 below shows additional methods for retrieving objects from a dictionary.
Method | Description |
---|---|
d.keys() |
Returns a dict-view of dictionary's keys |
d.values() |
Returns a dict-view of dictionary's values |
d.items() |
Returns a dict-view of dictionary's items (key , value ) pairs |
Let's create a dictionary to see how the above methods work.
>>> person = {
"name" : "John",
"age" : 26,
}
The d.keys()
returns all the dictionary keys, while the d.values()
return all the dictionary values. The d.items()
method returns all key-value tuple.
# Get all the keys of the dictionay
>>> person.keys()
dict_keys(['name', 'age'])
# Get all items in list of tuples
>>> person.items()
dict_items([('name', 'John'), ('age', 26)])
# Get all the values of the dictionary
>>> person.values()
dict_values(['John', 26])
The objects returned by dict.key()
, dict.values()
and dict.items()
are view objects. They provide a dynamic view of the dictionary objects, which means any changes to the source dictionary result in changes to the view objects.
What's the output of the following code?
>>> person = {
"name" : "John",
"age" : 26,
}
>>> a = person.keys() # Name the view object `a`
>>> a
dict_keys(['name', 'age'])
>>> person["weight"] = 125 # Dictionary modified
>>> a # Check the view again
dict_values(['John', 26])
dict_keys(['name', 'age'])
dict_keys(['name', 'age', 'weight'])
dict_values(['John', 26, 125])
As I mentioned earlier, the view objects are dynamic and provide the most updated view of the dictionary objects. However, often we want to work with common containers such as lists
and tuples
instead of view objects.
We can convert dictionary view objects to list, tuples, or sets using their corresponding constructors. Let's continue with the person
dictionary from the last exercise.
>>> list(person.items())
[('name', 'John'), ('age', 26), ('weight', 125)]
>>> set(person.keys())
('name', 'age', 'weight')
>>> tuple(person.values())
('John', 26, 125)
As you can notice, the view objects get converted to the corresponding containers.
The dictionary view object also supports membership operation. You can use the built-in len()
function to get the length.
>>> len(person.items())
3
>>> 'age' in person.keys()
True
>>> 126 not in person.values()
True
>>> ('age', 26) in person.items()
True
We can also use the membership and length operation directly on dictionaries. At the same time, while checking membership on dictionaries, python checks if the given object exists in the dictionary keys.
>>> len(person)
3
>>> "age" in person # 'age' exists in d.keys()
True
>>> 26 in person # 26 is present in values not keys
False
What's the output of the below code?
>>> scores = {
"A": 26,
"B": 21,
"C": 23
}
>>> "B" in scores , 26 in scores.values(), (23, "C") in scores.items()
(True, True, False)
(True, False, True)
(False, True, False)
(True, True, True)
The d.items()
returns items in the form of (key, value)
tuple. Next, let's look into how we can modify the dictionary items.
Modify and Remove Dictionary Items
Earlier, we reassigned dictionary items using curly square brackets. For example
>>> person = {"age" : 26} # Create a dictionary
>>> person["age"] = 36 # Assign the `age` key to 36 instead
>>> person # Dictionary modified
{'age': 36}
A dictionary object can also be updated using its built-in object method d.update()
. The update()
method accepts
- a keyword argument
- iterable of key/value pairs of tuples or other iterable of length two
Let's check out how a dictionary can be updated using an iterable consisting of key/value pairs. First, let's recreate the person
dictionary.
>>> person = {"name" : "John", "age" : 36, "weight": 125}
>>> person
{'name': 'John', 'age': 36, 'weight': 125}
Let's store the new key/value pairs.
>>> new_values = [('name', "James"), ('age', 28), ('weight', 136), ('city', 'Berlin')]
Now, let's modify the person
dictionary items using d.update()
method.
>>> person.update(new_values)
>>> person
{'name': 'James', 'age': 28, 'weight': 136, 'city': 'Berlin'} # Dictionary updated
The below code listing shows a dictionary can be updated using keyword arguments.
>>> person = {"name" : "John", "age" : 36, "weight": 125}
>>> person.update(name="John", age=28, weight=136, city="Berlin")
>>> person
{'name': 'John', 'age': 28, 'weight': 136, 'city': 'Berlin'} # Dict updated
The d.update()
method is useful to update as many items of a dictionary simultaneously.
Like previous containers, the items in a dictionary can be removed using the del
keyword and clear()
method. Let's take a look.
To remove a single item, we can use the del
keyword. For instance,
>>> person
{'name': 'John', 'age': 28, 'weight': 136, 'city': 'Berlin'}
>>> del person["city"] # Remove the key `city`
>>> person # `city` object removed
{'name': 'John', 'age': 28, 'weight': 136}
To remove every item stored in a dictionary, Python provides a built-in method, d.clear()
.
>>> person
{'name': 'John', 'age': 28, 'weight': 136}
>>> person.clear() # Remove all items stored
>>> person
{}
One obvious way is to assign the new key to the value stored in the old key in the dictionary and then remove the old key. However, there is a much better way which we will look at next.
Retreive and Modify Dictionary
Similar to sequences, dictionaries in Python also have the d.pop(<key>, <default>)
method, which retrieves while removing the item at the same time.
>>> d = {1: 'Apple', 2: 'Ball', 3: 'Carrots' }
>>> d.pop(2)
'Ball'
>>> d
{1: 'Apple', 3: 'Carrots'}
If a key is not found in the dictionary, then the method pop()
raises KeyError
.
>>> d.pop(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 4
The pop()
method accepts a default argument which can be returned instead of Exception
when a key is not found in the dictionary.
>>> d.pop(5, "Key is not present.")
'Key is not present.'
Like the method pop()
, dictionary objects also have a method d.popitem()
, which returns and removes an arbitrarily selected item from the dictionary.
>>> d = {1:"Apple", 2: "Ball", 3:"Carrots", 4:"Dogs"}
>>> d.popitem()
(4, 'Dogs')
>>> d
{1: 'Apple', 2: 'Ball', 3: 'Carrots'}
If the dictionary is empty, the popitem()
method raises KeyError
.
d.pop
and d.popitem()
, can you think of a better way to rename a key in the Python dictionary.
We can rename a key by using : d["new_key"] = d.pop("old_key")
. The pop()
method returns the value while deleting the item at the same time.
So far, we covered the different built-in types of collections objects, such as - sequences, mappings, and set objects. The built-in collections module also provides several collections objects which can be used as container objects. Let's take a look.
Specialized Containers
Some of the specialized container type presents are as follows:
- Namedtuple is useful for creating tuple subclasses with named fields
- Deque is a list-like container that can pop on either end
- ChainMap is a dictionary-like object for creating a single view of multiple mappings
- Counter is used for counting hashable objects
- Defaultdict is like a dictionary, but unspecified keys have a user-specified default value
You can read more about their usage in the official python documentation of the collections module.
Next, let's look into another category of built-in types: Callables.
Callables
A callable is anything you can call using parenthesis and optionally passing arguments.
Functions and Methods are some of the callables that we have seen.
Callable types are objects that support the function call operation. The list of callable type in Python is as follows:
- User-defined functions
- Built-in methods
- Built-in functions
- Generators
- Class Instances
- Classes
- Instance Methods
In the previous chapter, we covered functions and methods in general. In this course, we won't be covering details about classes and instances.
We will learn about Generators in detail in Chapter 6.
Apart from these types, Python has several other types that are used for internal purposes. We would not be covering those.
In the next chapter, we will dive deep into the Program Structure and Control Flow.
