Data and Operations
One of the best ways to begin your journey as a programmer is to understand how data is created, stored and manipulated. In this chapter, you will learn how to initialise variables, work with constants, and work with literals. We'll go through the different types of data in Python. You'll experiment with different operations, and manipulate variables using operators. Finally, you'll be introduced to the concept of Objects, and learn about mutable and immutable objects in Python.

Buckle up, this is going to be so much fun!
2.1: Variables and Constants
Variables - in any programming language - are like lockers where trophies are kept. They have an identification number to locate the trophy locker and a nameplate to help identify whom they belong to. In Python, variables are initialized by assigning a value to a name. This way, the value is stored as data in a computer memory location. The name acts as a nameplate, and the memory location acts as the identification number telling you where the locker is on the wall.

NOTE: Before you start, remember to launch the Python shell in your CMD!

Let's create a variable annet, which is a locker belonging to a girl called "Annet". Suppose, Annet has won a trophy ranking '10'. Here's how we'll represent it in Python:

>>> annet = 10               

Now, if you want to check what is inside this locker, just type the name as shown below:

>>> annet

And if you want to check the identification number for this locker, you type:

>>> id(annet)      # You will get a different ID from mine, so don't worry        

This shows that the variable 'annet' is stored at the memory location 140713851540800. Now, let's consider another girl 'Diana', for whom we create a variable diana. Diana too has won the trophy ranking '10'. Like before, let's create the variable and check it's value and id:

>>> diana = 10
>>> diana
>>> id(diana)

If you look closely, you'll see that annet and diana share the same locker identification number (memory location), since they share the same trophy ranking 10. When we create variables in Python that have the same value, the data for these variables are stored in the same memory location. This means that both variables point to the same memory location. But what happens when you change the value of one of the variables, say annet?

>>> annet = 6       # annet has now been given a trophy ranking 6
>>> annet           # Let's check the value of annet now
>>> id(annet)       # What about the memory location?
>>> id(diana)       # And does diana's memory location change? Nope!

This shows that annet and diana are two different variables that pointed to the same value stored at a location. As the value of the variable changes, the location it points to changes as well.

Constants are variables whose value cannot be changed. And the newsflash is that Python does not allow us to create constants. However, there are other ways to create constants, which we will see as we progress in this course.

I leave this up to you: Change the value of annet back to 10 and check its memory location as we have done in the above lines of code. What do you get?
2.2: Literally Speaking
In the examples given in the previous section, we created two variables annet and diana that contained integer numbers. These numbers belonged to the decimal number system. However, what if you wanted to assign values that were, let's say, binary?

Let's create a variable, binaryVariable001 which holds a binary value 1010, which is equal to 10 in the decimal number system. Let's also create another variable binaryVariable002, which holds a binary value 1000, which is equal to 8 in the decimal system.

>>> binaryVariable001 = 1010
>>> binaryVariable002 = 1000

Now, what if we add these two?

>>> binaryVariable001 + binaryVariable002

Wait! Something's wrong. The addition of binary 1010 and 1000 should be 010010, so why did we get 2010? Well, that's because the numbers entered were in the decimal system and not the binary system. In order to deal with this, we work with literals while programming.

Literals - used in most programming languages - are variables that store data in raw formats for the sake of representation and calculations. Python allows us to work with Numeric Literals, Character Literals, Special Literals and Boolean Literals. For now, we'll only look at numeric and character literals.

Numeric Literals allow you to store values for binary, decimal, octal and hexadecimal number systems. Using these, you can add, subtract, multiply, and divide numbers belonging to these number systems. Have a look at the code below:

>>> binaryVariable001 = 0b1010             # Binary value 1010, decimal is 10
>>> binaryVariable002 = 0b1000             # Binary value 1000, decimal is 08
>>> binaryVariable001 + binaryVariable002
>>> # Use function bin() to get the binary value of binaryVariable001 + binaryVariable002
>>> bin(binaryVariable001 + binaryVariable002)
>>> hexVariable001 = 0x28                  # Hexadecimal value 28, decimal is 40
>>> hexVariable002 = 0x3C                  # Hexadecimal value 3C, decimal is 60
>>> hex(hexVariable001 + hexVariable002)
>>> # Use function hex() to get the hexadecimal value of hexVariable001 + hexVariable002
>>> hex(hexVariable001 + hexVariable002)

Don't be worried by this introduction of functions like bin() and hex() in the above example. Functions are blocks of code that are defined and used to carry out specific actions and operations. In this case, we are using predefined Python functions. We will explore functions in detail at a later stage in this course. You will also learn how to create your own functions and use them in your programs!

How 'bout you try out octal literals by yourself? Follow the same steps as shown above, and use the function oct() to convert the result of addition from decimal to the octal system. Choose any two octal numbers you'd like! Octal numbers are written in the form '0o20'.

Character literals allow you to include special characters in your sentences and words in Python. We've already seen how to assign numeric values to variables. Now, let's create a variable greetings which stores a string of characters:

>>> greetings = "Hello there! Welcome to Python cafe."
>>> greetings
'Hello there! Welcome to Python cafe.'

Let's say, you want to type the word café instead of cafe (notice to é). This can be done in Python by using unicode character literals. For more information on unicode characters literals, check this link.

Here's how we'll assign the string in Python in order to use character literals:

>>> greetings = "Hello there! Welcome to Python cafe."
>>> greetings
'Hello there! Welcome to Python cafe.'
>>> # Replace 'e' in the word 'cafe' with unicode value '\u00E9' 
>>> unicodeGreetings = u"Hello there! Welcome to Python caf\u00E9"
>>> unicodeGreetings
'Hello there! Welcome to Python café.'
2.3: Playing with Numbers in Python
Data types in Python represent the type of data that is stored in a variable or other data objects. Python, like all languages, has a variety of data built-in data types - Numbers, Strings, Tuples, Arrays, etc. There's a lot more.

We'll dive into Numbers first. These are divided into a variety of different sub-types, called Integers, Booleans, Reals, Complex, Fractions, and Decimals.

Let's start with Integers...

the fun thing about Integers in Python is that they have unlimited range. This means that as long as you have virtual memory available in your allocated virtual environment, you're good to use an Integer number that drags on to whatever length you please. In short, there is virtually no maximum number that you can assign to a variable in Python, as long as you have the memory to store it in.

An Integer number can be either positive, negative, or a zero (0). Have a look at the examples below:

>>> a = 5
>>> b = -4                

Here, we have assigned values to two Integer variables. Python implicitly considers a variable assigned a number value as Integer, unless explicitly stated. Using the function type(), you can check the data type of a variable.

>>> type(a)    # Check the data type of 'a'
<class 'int'>
>>> type(b)    # Check the data type of 'b'  
<class 'int'>          

Try out basic arithmetic operations with integers:

>>> # Integer Addition 
>>> aPlusB = a + b                 # Add 'a' and 'b' and save the result in a new variable     
>>> print('A + B =', aPlusB)       # Print the result stored in 'aPlusB' along with a message
A + b = 1
>>> # Integer Subtraction 
>>> aMinusB = a - b                # Subtract 'b' from 'a' and save the result in a new variable     
>>> print('A - B =', aMinusB)      # Print the result stored in 'aMinusB' along with a message
A - B = 9
>>> # Integer Multiplication 
>>> aIntoB = a * b                 # Multiply 'a' and 'b' and save the result in a new variable     
>>> print('A x B =', aIntoB)       # Print the result stored in 'aIntoB' along with a message
A into B = -20
>>> # Integer Division 
>>> aByB = a / b                   # Divide 'a' by 'b' and save the result in a new variable     
>>> print('A / B =', aByB)         # Print the result stored in 'aByB' along with a message
A divide by B = -1.25

You see that Python successfully carries out basic arithmetic operations on integers following arithmetic laws of positive and negative numbers.

Here's some more basic stuff that can be done with Integers:

>>> aSquared = a ** 2
>>> print('A squared =', aSquared)
A squared = 16
>>> # Now, using the power operation, I'd like to demonstrate how Python provides an unrestricted
>>> # range for the value and length of Integer numbers.
>>> aRaisedTo1024 = a ** 1024
>>> # Be ready to have your mind blown!
>>> print('A raised to 1024 =', aRaisedTo1024)
>>> print('Length of the resulting number =', len(str(aRaisedTo1024)))
Length of the resulting number = 716

Did your computer explode?! I am sure your brain did. Computers (and Python) are awesome, ain't it?

NOTE: In the above example, in the last line, we first converted the result value from a number to a string of characters, and then calculated the length of the string.

There's even more Integer operations that you can carry out in Python. We saw regular division which produces a number with a decimal point. You can also carry out rounded division which rounds off the result to the lower integer value:

>>> c = 12
>>> d = 8
>>> # Rounded of Division
>>> cByDRounded = c // d
>>> print('C // D =', cByDRounded)     # c / d = 1.5, rounded to 1
>>> dByCRounded = d // c
>>> print('D // C =', dByCRounded)     # d / c = 0.666666666666... rounded to 0

How about we use rounded division on a negative result? Let's divide a by b.

>>> aByBRounded = a // b
>>> print('A // B =', aByBRounded)     # a / b = -1.25, rounded to -2, the lower integer value

This is called flooring, because it rounds off the result to the lower limit of a range of possible values.

The modulus operation is an interesting operation that returns the remainder of an integer division. Consider the modulus of 100 divided by 7.

>>> e = 100
>>> f = 7
>>> # Modulus operation
>>> eModuloF = e % f
>>> print('E % F =', eModuloF)     # Print the remainder of 'e' divided by 'f' along with a message
2.4: Reals, Complex Numbers and Decimals
Real Numbers are also called Floating Point Numbers. For a layman's definition, floating point numbers consist of decimal points, with one or more digits after the decimal point.

For a more technical definition, I'm going to have you take a look at the following terms, online:
  1. IEEE 754 double precision binary floating-point format
  2. Single Precision numbers - These take up 32 bits of memory for each declaration
  3. Double Precision numbers - These take up 64 bits of memory for each declaration

Python makes use of double precision when it comes to real numbers, which means that each floating-point variable is allocated 64 bits of memory, allowing us to store up to 2 to the power 64 numbers with that amount of bits...

>>> floatingPointLimit = 2 ** 64   
>>> print('Largest Floating-Point Number =', floatingPointLimit);

Well, that's a whopping 18,446,744,073,709,551,616 numbers with that amount of bits.

Anyway, what's precision about? And why are talking about it?

Well, precision is basically the amount of significant digits required to represent a number. For example, 2.54432581 is better than simple 2.54. You see, the former provides more precision than the latter. 2.54432581 is closer to providing accuracy than simply 2.54. This is really important from a scientific standpoint, and since Python was built keeping the scientific community in mind, it only makes sense that Real Numbers would have better precision.

Alright then! Let's explore real numbers:

>>> # Calculate the area of a circle with radius '6'
>>> pi = 3.14159265
>>> radius = 6
>>> print('Area of the circle:', pi * (radius ** 2))

Note the use of brackets in calculating the area of the circle. Also, we have calculated the area of the circle within the print() function, instead of first saving it in a variable and then using that variable in the print() function. This way we have reduced the amount of time required to execute the entire code.

You can carry out all integer operations on real numbers. Try it out for yourself!

Fractions are variables that hold a rational numerator and denominator in their lowest possible forms. Let's look at a few examples:

>>> from fractions import Fraction     # Import the module 'Fraction' from the package 'fractions'
>>> numerator001 = 15
>>> denominator001 = 9
>>> fraction001 = Fraction(numerator001, denominator001)
>>> print('Fraction 15/9 =', fraction001)
Fraction(5, 3)

Is it possible to carry out integer operations on two different fractions? Yes, it is!

>>> fractionAddition = Fraction(15, 9) + Fraction(7, 14)
>>> print('(15 / 9) + (7 / 14) =', fractionAddition)
(15 / 9) + (7 / 14) = Fraction(13, 6)               

You can also add or subtract or divide or multiply a fraction with an integer or real number:

>>> fractionIntegerAddition = Fraction(15, 9) + 20
>>> print('(15 / 9) + 20 =', fractionIntegerAddition)
Fraction(65, 3)
>>> fractionRealAddition = Fraction(15, 9) + 3.14159265
>>> print('(15 / 9) + 3.14159265 =', fractionRealAddition)

Addition of a fraction and an integer results in a fraction, while addition of a fraction and real number results in a real number. This is because the order of precedence for the three types is reals > fractions > integers.

Fractions may be very useful in some applications, but you'll not find them being used often. This is simply because it is better to use the Decimals data type if you're looking for values that provide you with precision in scientific and financial calculations.

Decimals are variables that can represent floating point numbers exactly. Regular floating point numbers in Python are often victims of approximation issues, which could spell doom while performing calculations in scientific applications. For example, have a look at the following floating point calculations:

>>> 1.1 + 2.2          # Expected answer: 3.3
>>> 11.11 + 12.12      # Expected answer: 23.23
>>> (6 * 0.1) - 0.6    # Expected answer: 0.0

You can see how a higher precision in real numbers can end up erroneous. To work around this, Python provides the decimal package which contains the module Decimal. We could fix all the above errors by simply creating Decimal variables:

>>> from decimal import Decimal
>>> Decimal('1.1') + Decimal('2.2')    # The numbers are passed to the Decimal module as character strings inside single apostrophes
>>> Decimal('11.11') + Decimal('12.12')
>>> (Decimal('6') * Decimal('0.1')) - Decimal('0.6')
2.5: True or False: Booleans
If you're an engineer, or from the STEM background, you'll know what Booleans are. Boolean entities, in the most basic definition, cater to truth values: True or False. As a sub-class of Integers, they can also be represented as 1 or 0. Or more simply as any non-zero number and a zero number.

In Python, every variable can be respresented in the Boolean form using the bool() function.

Let's look at some quick, simple examples on how Boolean variables work:

>>> a = True        # Initialize a variable 'a' as 'True'
>>> b = False       # Initialize a variable 'b' as 'False'               

The integer equivalent of a Boolean 'True' is '1', while of a Boolean 'False' is '0'. Thus, Booleans can be used in operations along with integers and real numbers:

>>> a + 24     # 1 + 24 = 25
>>> 3 - b      # 3 - 0 = 3  
>>> a * b      # 1 * 0 = 0

Try some operations for yourself by experimenting with Booleans and real numbers.

They can even be operated upon with Decimals and Fractions, if you believe me!

>>> from fractions import Fraction     # Don't import this if you already have during this session
>>> a + Fraction(10, 20)               # 1 + Fraction(10, 20)
Fraction(3, 2)

Now, let's have a look at operations specific to Boolean variables:

>>> # Logical AND
>>> True and True
>>> True and False
>>> # Logical OR
>>> True or False
>>> False or False
>>> # Logical NOT
>>> not True
>>> not False

Well, since we've seen arithmetic operations applied to Boolean variables, it only makes sense to ask: Can we apply Boolean operations to integers and real numbers?

>>> 12 and 16      # The result is the second among both integers
>>> 12 or 16       # The result is the first among both integers
>>> 12 or -10      # The result is the first among both integer
>>> 10 or 0        # The result is always the non-zero integer if one is '0'
>>> 0 and -4       # The result is always zero if one of the integers is '0' 
>>> not 1          # The result of negation of a non-zero number is 'False'
>>> not 0          # The result of negation of a zero number is 'True'

How about a combination of integer and Boolean operations? Try them out, like this one:

>>> True + 5 or False
>>> (5 - (not 6)) and True
2.6: Putting Words Together: Characters and Strings
Let's start with defining Strings in Python. Till now, we've looked at different numerical types. Strings are basically just a sequence of textual data (characters, which may include numbers).

String variables can be initialized in different ways in Python:

>>> # A string sequence can be initialized using single quotes (' ')                 
>>> firstName = 'Kevin'
>>> # A string sequence can be initialized using double quotes (" ")
>>> lastName = "Sequeira"
>>> print('First Name:', firstName)
First Name: Kevin
>>> print('Last Name:', lastName)
Last Name: Sequeira

String sequences can be concatenated to form larger strings. A close look will tell you that it doesn't matter if the individual String sequences were initialized differently.

>>> fullName = firstName + ' ' + lastName
>>> print('Full Name:', fullName)
Kevin Sequeira

Notice the blank space (' ') that we addded between the two strings so that we could distinguish between the two strings after addition.

String sequences can also be initialized using triple single quotes ('''   ''') and these can span multiple lines of code.

>>> description = '''Hey everyone! I'm a Data Scientist from Mumbai, India.
... I've formerly been a Developer at a leading MNC, and an Editor at a leading Publishing House,
... and I'm interested in Python, Java, R, and various front-end technologies!'''                

Here's how the output would look:

>>> print('Description:', '\n', description)
 Hey everyone! I'm a Data Scientist from Mumbai, India.
I've formerly been a Developer at a leading MNC, and an Editor at a leading Publishing House,
and I'm interested in Python, Java, R, and various front-end technologies!         

Note two things in the above code snippet. First, we have used a special character \n which sends the program to the next line, and then prints the remainder of the print statement. Here, the program first prints 'Description:', goes to the next line, and then prints the multi-line string stored in description.

The second thing I want you to note is the single space in the first line of the string output ( Hey everyone! I'm a Data Scientist from...). This is because of the spaces between the commas in the print() function. The spaces after commas introduce spaces while printing the output. To avoid this, you can use another version of printing, as shown below:

>>> print('Description:' + '\n' + description)
Hey everyone! I'm a Data Scientist from Mumbai, India.
I've formerly been a Developer at a leading MNC, and an Editor at a leading Publishing House,
and I'm interested in Python, Java, R, and various front-end technologies!                     

Using a plus ( + ) instead of comma ( , ) helps to avoid unnecessary commas in the output, however we can no longer print numbers and words together without first converting these numbers to strings. So, here's what we need to do:

>>> age = 26
>>> print('Age: ' + str(age) + ' years')        
Age: 26 years            

Notice the blank space at the end of 'Age: ' and the beginning of ' years' inside the print() function. These have to be added manually, whenever you want to print spaces within strings for output messages.

Coming back to strings, you can also create multi-line strings using triple double quotes ("""   """). I'll leave it up to you to try it out.

A better definition of Strings would be: A sequence of characters that are positionally ordered ordered, and has a finite length once defined.

Yes, strings are a sequence of characters with postion numbers assigned to these characters. These position numbers, known as indexes are ordered from left-to-right, like a number line starting from zero (0). Consider the string fullName that we created earlier:

>>> print('Last Name: ' + lastName)
>>> print('First Charachter of Last Name: ' + fullName[0])     # Index value = (position = 1) - 1
>>> print('Last Character of Last Name: ' + fullName[7])       # Index value = (position = 8) - 1

The first character of the string is identified by index value 0. Naturally, the last character is identified by the index value (number of characters in the string - 1).

Try extracting and printing the third and eleventh characters of the string fullName. What do you get?

>>> print('Full Name: ' + fullName)
Full Name: Kevin Sequeira
>>> print('Third Charachter of Full Name: ' + fullName[2])
>>> print('Eleventh Character of Full Name: ' + fullName[10])

If you look at the code and the output carefully, you'll see that even the blank space ('  ') is considered a character. This is called an blank character.

Strings can be sliced with the help of index values. For example, the stringfullName can be sliced into two different strings. Try this:

>>> # Slice 'fullName' from index position '0' to index position '5 - 1'                
>>> firstSlice = fullName[0:5]
>>> # Slice 'fullName' from index position '6' to index position '14 - 1'
>>> secondSlice = fullName[6:14]
>>> print('First Name: ' + firstSlice)
First Name: Kevin
>>> print('Last Name: ' + secondSlice)
Last Name: Sequeira

String slicing is pretty simple. In fact, there's a cooler way to carry out the above indexing operations. Check this out:

>>> # Slice 'fullName' from index position '0' to index position '5 - 1'                
>>> firstSlice = fullName[:5]
>>> # Slice 'fullName' from index position '6' to index position '14 - 1'
>>> secondSlice = fullName[6:]
>>> print('First Name: ' + firstSlice)
First Name: Kevin
>>> print('Last Name: ' + secondSlice)
Last Name: Sequeira                

Did you notice that the indexing for firstSlice and secondSlice are slightly different than before? This is because if you want slice a string from the 0th index to a point in between, you don't need to mention the 0-index position. Similarly, if you want to slice a string form somewhere in between uptil the last character of the string, you don't need to mention the final-index of the string.

Take another quick example, where we want to slice the phrase "all day long" into three parts. Using the above idea, here's what we can do:

>>> allDayLong = 'all day long'
>>> # Slice the first word out of the phrase
>>> allSlice = allDayLong[:3]
>>> # Slice the second word out of the phrase
>>> daySlice = allDayLong[4:7]
>>> # Slice the third word out of the phrase
>>> longSlice = allDayLong[8:]
>>> # Print the three words together
>>> print('Sliced Words: ' + allSlice, daySlice, longSlice)
Sliced Words: all day long               

NOTE: Note how we've merged the two styles of printing together. We've used both, the plus sign and the comma sign in this example.

You can merge slicing and concatenation to create totally new strings. Check this:

>>> angTheAvatar = 'The Last Airbender'
>>> lukeSkywalker = 'Jedi'
>>> # Time for a Crossover
>>> print('Movies that were underrated: ' + angTheAvatar[:10] + lukeSkywalker)
Movies that were underrated: The Last Jedi                

Apart from these basic string operations, there are three string-specific functions that I would like to demonstrate here: find(), split(), and replace().

Using find(), you can search for the occurrence of a character, or a string of characters, within another string. For example, take consider the sentence 'My way or the highway. You can use the find() function to locate the index position of the first occurrence of the word 'way':

>>> sentence = 'My way or the highway'
>>> print('First occurrence of \'way\' at index position', sentence.find('way'))
First occurrence of 'way' at index position 3                

The find() function finds the substring 'way' first at index position 3.

In the print() function in the above lines of code, notice the two backslashes (' \ '). These are used so that the print() function doesn't confuse the single quotes around the word 'way' with the opening and closing single quotes for the print message. In this case \' is a special character.

One way to avoid this is to use double quotes for the print message, like this:

>>> print("First occurrence of 'way' at index position", sentence.find('way'))
First occurrence of 'way' at index position 3            

We'll revisit special characters in an chapter during a later stage.

With split() you can split a string into a list of two or more substrings. Splitting is done by identifying a character or substring of characters within a string, and splitting the string around the mentioned character(s). Take an example where you want to split the phrase 'day-in-day-out' at the hyphens ( - ):

>>>  phrase = 'day-in-day-out'
>>>  splitWords = phrase.split('-')
>>>  print('List of words after split:', splitWords)   
List of words after split: ['day', 'in', 'day', 'out']           

On splitting, the words are saved as a list of strings. We will explore lists in detail in Chapter 4. Remember, that the string phrase in itself is not affected, and it remains the same. Only the result of the function split() is returned as a list which needs to be stored in a new variable.

The function replace() allows you to replace a character or a sequence of characters in a string, with a substring of your choice. Say, for example, you want to replace the subtring 'man' in 'Batman' to 'woman'. Here's how to do it with replace():

>>> batman = 'Batman'
>>> batwoman = batman.replace('man', 'woman')
>>> print(batman + ' and ' + batwoman)
Batman and Batwoman                

I'll leave this up to you: try replacing a entire substring in a sentence with a single character, or the other way around. What happens?