Learn Python Programming From Beginner to Pro

If you have an interest in Data Science, Web Development, Robotics, or IoT you must learn Python. Python has become the fastest-growing programming language due to its heavy usage and wide range of applications.

For a beginner or a person from a non-tech background, learning Python is a good choice. The syntax is like talking and writing plain English. For example, consider this syntax which shows its resemblance to the English language.

print("Hello folks")

We will use Python3 in this tutorial as it is widely used. Most of the frameworks and libraries support this version.

Note: Any version above 3.5.2 supports most of the libraries and frameworks.

Index:

  1. Introduction
  2. Installation
  3. Python shell
  4. Comment
  5. Print
  6. Indentation
  7. Variables
  8. Operators
  9. Conditional Statements
  10. For Loops
  11. While loops
  12. User Input
  13. Typecasting
  14. Dictionaries
  15. Lists
  16. Tuples
  17. Sets
  18. Functions and Arguments
  19. Args
  20. keyword Arguments
  21. Default Arguments
  22. kwargs
  23. Scope
  24. Return Statement
  25. Lambda Expression
  26. List comprehension
  27. OOPS concepts
  28. Classes
  29. Methods
  30. Objects
  31. Constructor
  32. Instance attribute
  33. Class attributes
  34. Self
  35. Inheritance
  36. Super
  37. Multiple Inheritance
  38. Polymorphism
  39. Encapsulation
  40. Decorators
  41. Exceptions
  42. Package Import
  43. JSON Handling

Note: The beginning of this guide is geared towards beginners. If you have intermediate experience in Python, you can skip to the part that interest’s you most using the links above.

Introduction:

As per Github’s octoverse, Python is the second most used language by developers in 2019.

Octoverse graph of how languages have evolved

Before learning any language it’s helpful to know how that language came into existence. Well, Python was developed by Guido van Rossum, a Dutch programmer, and was released in 1991.

Python is an Interpreted language. It uses the CPython Interpreter to compile the Python code to byte code. For a beginner, you don’t need to know much about CPython but you must be aware of how Python works internally.

The philosophy behind Python is that code must be readable. It achieves this with the help of indentation. It supports many programming paradigms like Functional and Object Oriented. We will understand them as we move down through the article.

The basic question that most beginners have in mind is what a language can do. Here are some of the use-cases of Python:

  • Server-side development ( Django, Flask )
  • Data Science ( Pytorch, Tensor-flow )
  • Data analysis / Visualisation ( Matplotlib )
  • Scripting ( Beautiful Soup )
  • Embedded development

Note: I do not endorse any of the above-mentioned libraries or frameworks in particular. They are popular and broadly used in their respective domains.

Installation

The first step of learning any programming language is to know how to install it. Python comes bundled into most of the operating systems nowadays. Use the following command in your terminal to check if Python is available:

python3 --version

You’ll see the following output:

Python 3.7.0

If you have many versions of Python available in your system then you may see a different version. If you have Python installed and the version is above 3.5.2 then you can skip this section.

For those who don’t have python installed, follow the steps below:

  • Windows User
  • Mac User
  • Linux User

Windows User:

  • Go to Python’s official website.
  • Click on the download button ( Download Python 3.8.2 ) [ Note: The version may differ based on when you are reading this article ]
  • Go to the path where the package is downloaded and double-click the installer.
  • Check the box indicating to “Add Python 3.x to PATH” and then click on “Install Now”.
  • Once done you’ll get a prompt that “Setup was successful”. Check again if python is configured properly using the above command.
  • To confirm if python is installed and configured properly used the command python3 --version.

Mac User:

  • First install xcode from the app store.
  • If you want to install Xcode using the terminal then use the following command:
xcode-select --install

  • After that, to install python we will use the brew package manager. To install and configure brew use the following command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

  • Once brew setup is done use the following command to update any outdated package:
brew update
  • To install python use the following command:
brew install python3
  • To confirm if python is installed and configured properly used the command python3 --version.

Linux User:

  • To install python using apt, use the following command:
sudo apt install python3

  • To install the python using yum, use the following command:
sudo yum install python3

  • To confirm if python is installed and configured properly used the command python3 --version.

Python shell:

The shell is one of the most useful tools you’ll come across. The python shell gives us the power to quickly test any concept before integrating it into our application.

Go to the terminal or command line prompt. Enter python3 command and you’ll get the following output:

➜ python3.7
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

In this tutorial, we will learn some concepts with the help of the python3 shell which you can see above. From now on, whenever I mention go to the python shell it means that you have to use the python3 command.

To learn the remaining concepts we will create a file with an extension .py. To run this file we use the following command:

python3 testing.py

Let’s go to the python shell. Type 10 + 12 after this >>> mark. You’ll get the output of 22:

>>> 10 + 12
22

Commenting:

Comments make it easy to write code as they help us (and others) understand why a particular piece of code was written. Another awesome thing about comments is that they help improve the readability of the code.

# Stay Safe

When you add the above syntax, the python interpreter understands that it is a comment. The lines inside this are not executed.

You may be wondering why you should use comments. Imagine you are a developer and you have been assigned to a huge project. The project has more than a thousand lines of code. Now to understand how the code works you’ll need to go line by line and understand it.

What’s a better solution than that? Ah-hah! Here come the comments. The comments help us understand why a particular piece of code was written and what it returns or does. Consider it as documentation for every piece of code.

Print:

Believe me, other than debugging tools from the editor, the thing which helps developers solve problems most often is a print statement. The print statement is one of the most underrated pieces of syntax of programming. You will find that it will be most helpful when debugging any problems.

So how does it help in debugging an issue? Well, consider that you have a module and you want to check the flow of execution to understand it or debug a bug. There are two options. Either you can use a debugger or add a print statement.

It’s not always possible that you can set a debugger. For example, if you are using a python shell, then a debugger is not available. In such a scenario, print helps us. Another scenario is when your application is running you can add a print statement that will display in the logs of your application. You can monitor them in runtime.

Python provides a inbuilt print method with the following syntax:

print("Stay safe...")

Indentation:

Another interesting part of this language is indentation. Why? Well, the answer is simple: It makes the code readable and well-formatted. It is compulsory in python to follow the rules of indentation. If proper indentation is not followed you’ll get the following error:

IndentationError: unexpected indent

See, even the errors in python are so readable and easy to understand. At the start, you may be annoyed by the compulsion of indentation. But with the time you’ll understand that indentation is a developer’s friend.

Variables:

As the name implies, a variable is something that can change. A variable is a way of referring to a memory location used by a computer program.

Well in most programming languages you need to assign the type to a variable. But in python, you don’t need to. For example in the C language for declaring an integer, the following syntax is used int num = 5;, while in python it’s num = 5 .

Go to the python shell and perform the operation step by step:

  • Integer: As the name suggests they are nothing but numerical values that can be positive, negative, or zero without a decimal point.
>>> num = 5
>>> print(num)
5
>>> type(num)
<class 'int'>

As you can see here we have declared a num variable and assigned 5 as a value. Python’s inbuilt type method can be used to check the type of variable. When we check the type of num we are seeing output <class 'int'>. For now, just focus on the int in that output. Int represents an integer.

  • Float: It is the same as an integer but with one slight difference, a numerical value with a decimal place.
>>> num = 5.0
>>> print(num)
5.0
>>> type(num)
<class 'float'>

Here we have assigned a number with a single decimal to the num. When we check the type of num we can see it is float.

  • String: It is nothing but a formation of characters or integers. They can be represented using double or single quotes.
>>> greet = "Hello user"
>>> print(greet)
Hello user
>>> type(greet)
<class 'str'>

Here we have assigned a string to greet. The type of greet is a string as you can see from the output.

  • Boolean: They are a binary operator with a True or False value.
>>> is_available = True
>>> print(is_available)
True
>>> type(is_available)
<class 'bool'>

Here we have assigned a True value to is_available. The type of this variable is boolean. You can only assign True or False. Remember T and F should be capital or it will give an error as follow:

>>> is_available = true
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'true' is not defined
  • NoneType: This is used when we don’t have the value of the variable.
>>> num = None
>>> print(num)
None
>>> type(num)
<class 'NoneType'>

Operators:

Refer the image below for understanding all the arithmetic operator available in python with there examples:

Operators table

Let’s understand the Operators one by one.

Arithmetic operators: Under this operator comes addition, subtraction, deletion, exponentiation, modulus, and floor division. Also the shorthand syntax.

First, we will declare two variables a and b.

>>> a = 6 # Assignment
>>> b = 2

Let’s try our basic arithmetic operations:

>>> a + b # Addition
8
>>> a - b # Subtraction
4
>>> a * b # Multiplication
12
>>> a / b # Division
3.0
>>> a ** b # Exponentiation
36

To test for other arithmetic operations let’s change the value of a and b.

>>> a = 7
>>> b = 3
>>> a % b # Modulus
1
>>> a // b # Floor division
2

Shorthand arithmetic operations are also available in python. Refer the image and you can test it the same way as above. To print, the output of shorthand operation use the print statement.

Comparison operators: Under this operator comes equal, greater than, less than.

>>> a = 5 # Assign
>>> b = 2 # Assign
>>> a > b # Greater than
True
>>> a < b # less then
False
>>> a == b # Equal to
False
>>> a >= 5 # Greater than or equal to
True
>>> b <= 1 # Less than or equal to
False

Logical operators: Under this operator comes not, and, or.

>>> a = 10
>>> b = 2
>>> a == 2 and b == 10 # and
False
>>> a == 10 or b == 10 # or
True
>>> not(a == 10) # not
False
>>> not(a == 2)
True

Conditional Statements:

As the name suggests conditional statements are used to evaluate if a condition is true or false. Many times when you are developing an application you need to check a certain condition and depending on that execution needs to done or you need to skip that part or terminate. In such scenarios conditional statements are useful. If, elif and else are the conditional statements used in python.

We can compare the variable, check if the variable has any value or if it’s a boolean then check if it’s true or false. Go to the python shell and perform the operation step by step:

Condition Number 1: We have an Integer and 3 conditions here. The first one is the if condition. It is checking if the number is equal to 10. The second one is the elif condition. Here we are checking if the number is less than 10. The last condition is else. This condition executes when none of the above conditions match.

>>> number = 5
>>> if number == 10:
...     print("Number is 10")
... elif number < 10:
...     print("Number is less than 10")
... else:
...     print("Number is more than 10")
...

Output:

Number is less than 10

Note: It is not compulsory to check that two conditions are equal in the if condition. You can do it in the elif also.

Condition Number 2: We have a boolean and 2 conditions here. Have you notice how we are checking if the condition is true. If is_available then print available else print not available.

>>> is_available = True
>>> if is_available:
...     print("Yes it is available")
... else:
...     print("Not available")
...

Output:

Yes it is available

Condition Number 3: Here we have reversed the condition number 2 with the help of not operator.

>>> is_available = True
>>> if not is_available:
...     print("Not available")
... else:
...     print("Yes it is available")
...

Output:

Yes it is available

Condition Number 4: Here we are declaring the data as None and checking if the data is available or not.

>>> data = None
>>> if data:
...     print("data is not none")
... else:
...     print("data is none")
...

Output:

data is none

Condition Number 5: You can also use inline if in python. The syntax to achieve this is the following:

>>> num_a = 10
>>> num_b = 5
>>> if num_a > num_b: print("num_a is greater than num_b")
...

Output:

num_a is greater than num_b

Condition Number 6: You can also use inline if else in python. The syntax to achieve this is the following:

expression_if_true if condition else expression_if_false

Example:

>>> num = 5
>>> print("Number is five") if num == 5 else print("Number is not five")

Output:

Number is five

Conditional Number 7: You can also use nested if-else in python. The syntax to achieve this is the following:

>>> num = 25
>>> if num > 10:
...     print("Number is greater than 10")
...     if num > 20:
...             print("Number is greater than 20")
...     if num > 30:
...             print("Number is greater than 30")
... else:
...     print("Number is smaller than 10")
...

Output:

Number is greater than 10
Number is greater than 20

Condition Number 8: You can also use and operator in a conditional statement. It states if condition1 and condition2 both are true then execute it.

>>> num = 10
>>> if num > 5 and num < 15:
...     print(num)
... else:
...     print("Number may be small than 5 or larger than 15")
...

Output:

10

As our number is between 5 and 15 we get the output of 10.

Condition Number 9: You can also use or operator in a conditional statement. It states that if either condition1 or condition2 is true then execute it.

>>> num = 10
>>> if num > 5 or num < 7:
...     print(num)
...

Output:

10

Are you confused because the value of num is 10 and our second condition states that num is less than 7? So why do we get the output as 10? It’s because of the or condition. As one of the condition matches it will execute it.


For Loops:

Another useful method in any programming language is an iterator. Consider you have to implement something multiple times what will you do?

print("Hello")
print("Hello")
print("Hello")

Well, that’s one way to do it. But imagine you have to do it for a hundred or a thousand times. Well, that’s a lot of print statements we have to write. Wait there’s a solution available. It is called iterators or loops. We can either use a for or while loop.

Here we are using the range method. It specifies the range till which the loop should be repeated. By default, the starting point is 0.

>>> for i in range(3):
...     print("Hello")
...

Output:

Hello
Hello
Hello

You can also specify the range in this way range(1,3).

>>> for i in range(1,3):
...     print("Hello")
...

Output:

Hello
Hello

See hello is only printed two times as we have specified the range here. Consider it has Number on right - Number on left equation.

Well, you can also add an else statement in the for loop.

>>> for i in range(3):
...     print("Hello")
... else:
...     print("Finished")

Output:

Hello
Hello
Hello
Finished

See our loop iterated 3 times ( 3 - 0 ) and once that is done it executed the else statement.

We can also nest a for loop inside another for loop.

>>> for i in range(3):
...     for j in range(2):
...             print("Inner loop")
...     print("Outer loop")
...

Output:

Inner loop
Inner loop
Outer loop
Inner loop
Inner loop
Outer loop
Inner loop
Inner loop
Outer loop

As you can see the inner loop print statement executed two times. After that outer loop print statement executed. Again the inner loop executed two times. So what is happening here? If you are confused then consider this to solve it:

  • Our Interpreter comes and sees ahh! there is a for loop. It goes down again and checks there is another for loop.
  • So now it will execute the inner for loop two times and exit. Once done with it then it knows that outer for loop has instructed it to repeat two more times.
  • It starts again and sees the inner for loop and repeats.

Well, you can also choose to pass a certain for loop condition. What does pass mean here? Well whenever that for loop will occur and the Interpreter sees the pass statement it will not execute it and move to the next line.

>>> for i in range(3):
...     pass
...

You will not get any output on the shell.


While loops:

Another loop or iterator available in python is while loop. We can achieve some results with the help of a while loop as we achieve with the for loop.

>>> i = 0
>>> while i < 5:
...     print("Number", i)
...     i += 1
...

Output:

Number 0
Number 1
Number 2
Number 3
Number 4

Remember whenever you use a while loop it’s important that you add an increment statement or a statement that will lead to end while loop at some point. If not then while loop will execute forever.

Another option is to add a break statement in a while loop. This will break the loop.

>>> i = 0
>>> while i < 5:
...     if i == 4:
...             break
...     print("Number", i)
...     i += 1
...

Output:

Number 0
Number 1
Number 2
Number 3

Here we are breaking the while loop if we find the value of i to be 4.

Another option is to add an else statement in while loop. The statement will be executed after the while loop is completed.

>>> i = 0
>>> while i < 5:
...     print("Number", i)
...     i += 1
... else:
...     print("Number is greater than 4")
...

Output:

Number 0
Number 1
Number 2
Number 3
Number 4
Number is greater than 4

The continue statement can be used to skip the current execution and proceed to next.

>>> i = 0
>>> while i < 6:
...     i += 1
...     if i == 2:
...             continue
...     print("number", i)
...

Output:

number 1
number 3
number 4
number 5
number 6

User Input:

Imagine you are building a command-line application. Now you have to take the user input and act accordingly. Here comes the python’s inbuilt method input.

Syntax to achieve this is as follows:

variable = input(".....")

Example:

>>> name = input("Enter your name: ")
Enter your name: Sharvin

When you use the input method and press enter. You’ll be prompted with the text that you enter in the input method. Let’s check if our assignment is working or not?

>>> print(name)
Sharvin

There it is!! it is working perfectly. Here Sharvin is of the type string.

>>> type(name)
<class 'str'>

Let’s try one more example where we will assign an integer rather than a string and check the type.

>>> date = input("Today's date: ")
Today's date: 12
>>> type(date)
<class 'str'>

Are you confused?? We entered an integer 12 and it’s still giving us its type as a string. It’s not a bug. It’s how input is intended to work. To convert the string to integer we will use typecast.

Typecasting:

We saw that the input method returns a string for the integer also. Now we want to compare this output with another integer then we need a way to convert it back to an integer. Here comes the typecasting into play.

>>> date_to_int = int(date)
>>> type(date_to_int)
<class 'int'>

Here we took the date that we have declared above in the User input section and converted it into the integer using the python’s inbuilt int method. This is called as typecasting.

Basically you can do the following conversion with the help of typecasting:

  • integer to string: str()
  • string to integer: int()
  • integer to float: float()

Note: Conversion from float to integer is also possible.

>>> type(date)
<class 'str'>

# Converting from string to float
>>> date_to_float = float(date)
>>> type(date_to_float)
<class 'float'>

# Converting from float to string
>>> date_to_string = str(date_to_float)
>>> type(date_to_string)
<class 'str'>

# Converting from float to integer
>>> date_to_int = int(date_to_float)
>>> type(date_to_int)
<class 'int'>

Dictionaries:

Imagine you want to store some user details. So How can you store these details? Yes, we can use variable to store them as follows:

>>> fname = "Sharvin"
>>> lname = "Shah"
>>> profession = "Developer"

To access this value we can access them as follows:

>>> print(fname)
Sharvin

But is this an elegant and optimized way to access it? The answer is no. To make it more friendly and store the data in a key-value dictionary comes to our rescue.

What is a dictionary? A dictionary is a collection that is unordered and mutable ( i.e. It can be updated ).

Following is the format of the dictionary:

data = {
	"key" : "value"
}

Let’s understand the dictionary further by an example:

>>> user_details = {
...     "fname": "Sharvin",
...     "lname": "Shah",
...     "profession": "Developer"
... }

Access value in dict: We can access the value inside a dictionary in two ways. We will first understand both the ways and then we will debug to find out which way is better.

Way_No_1:To access the value of fname key from user_details dictionary we can use the following syntax:

>>> user_details["fname"]
'Sharvin'

Way_No_2: We can also access the value of fname key from user_details dictionary using get.

>>> user_details.get("fname")
'Sharvin'

I know Way_No_1 looks easier to understand. The problem with this way is when we try to access the data that is not available in our dictionary.

>>> user_details["age"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'age'

We get a KeyError which indicates that the key is not available. Let’s try the same scenario with Way_No_2.

>>> user_details.get("age")

We do not get anything printed in our console. Let’s debug it further to know why this happened? Assign a variable age to our get operation and we will print it in our console.

>>> age = user_details.get("age")
>>> print(age)
None

Ahh!! So when get doesn’t find the key it sets the value to None and due to this, we do not get any error. Now you may be wondering which one is right? Most of the time usage of Way_No_2makes more sense but for some strict checking condition, we need to use Way_No_1.

Check if key exists: You may be wondering how to check if the dictionary as a particular key or not in python. Python provides a built-in method keys() to solve this issue.

>>> if "age" in user_details.keys():
...     print("Yes it is present")
... else:
...     print("Not present")
...

We will get the following output:

Not present

What if we want to check if the dictionary is empty or not? To understand this let’s declare an empty dictionary as follows:

>>> user_details = {}

When we use if-else on a dictionary directly it either returns true if data is present or false if empty.

>>> if user_details:
...     print("Not empty")
... else:
...     print("Empty")
...

Output:

Empty

We can also use python’s inbuilt method bool to check if the dictionary is empty or not. Remember bool returns False if the dictionary is empty and True if it is filled.

>>> bool(user_details)
False

>>> user_details = {
...     "fname" : "Sharvin"
... }
>>> bool(user_details)
True

Update value of existing key: So now we know how to get a particular key and find if it exists but how to update it in the dictionary?

Declare a dictionary as follows:

>>> user_details = {
...     "fname":"Sharvin",
...     "lname": "Shah",
...     "profession": "Developer"
... }

To update the value use the following syntax:

>>> user_details["profession"] = "Software Developer"
>>> print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer'}

Updating a value of key in dictionary is same as assigning a value to the variable.

Adding key-value pair: The next question is how to add a new value to the dictionary? Let’s add an age key with a value of 100.

>>> user_details["age"] = "100"
>>> print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer', 'age': '100'}

As you can see a new key-value is added in our dictionary.

Removing key-value pair: To remove a key-value from the dictionary python provides an inbuilt method called pop.

>>> user_details.pop("age")
'100'

>>> print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer'}

This removes the age key-value pair from the user_details dictionary. We can also use a del operator to delete the value.

>>> del user_details["age"]

>>> print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer'}

The del method can also be used to delete complete dictionary. Use the following syntax to delete complete dictionary del user_details.

Copy a Dictionary: A dictionary cannot be copied in a traditional way. For example, you cannot copy value of dictA to dictB as follows:

dictA = dictB

To copy the values you need to use the copy method.

>>> dictB = user_details.copy()

>>> print(dictB)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer'}

Lists:

Consider you have a bunch of data that is not labeled ( i.e. It doesn’t have a key that defines it ) So How will you store it? And here comes Lists to our rescue. They are defined as follows:

data = [ 1, 5, "xyz", True ]

A list is a collection of random, ordered, and mutable data ( i.e. It can be updated ).

Access list elements: Let’s try to access the first element:

>>> data[1]
5

Wait what happened here? We are trying to access the first element but we are getting the second element. Why? Indexing of the list begins from zero. So what do I mean by this? The indexing of the position of the elements begins from zero. The syntax to access an element is as follows:

list[position_in_list]

To access the first element we need to access it as follows:

>>> data[0]
1

You can also specify a range to access the element between those positions.

>>> data[2:4]
['xyz', True]

Here first value represents the start while the last value represents the position till which we want the value.

Add an item to list: To add an item in the list we need to use the append method provided by python.

>>> data.append("Hello")

>>> data
[1, 5, 'abc', True, 'Hello']

Change value of item: To change the value of an item, use the following syntax:

>>> data[2] = "abc"

>>> data
[1, 5, 'abc', True]

Remove item from list: To remove an item from a list we can use the python’s inbuilt remove method.

>>> data.remove("Hello")
>>> data
[1, 5, 'abc', True]

Loop through a list: We can also loop through the list to find a certain element and operate on it.

>>> for i in data:
...     print(i)
...

Output:

1
5
abc
True

Check item exists or not: To check if a particular item exists or not in list we can use if loop as follows:

>>> if 'abc' in data:
...     print("yess..")
...
yess..

Copy: To copy a list data from one list to another we need to use copy method.

>>> List2 = data.copy()
>>> List2
[1, 5, 'abc', True]

Length: We can also check the length of list using Python’s inbuilt len method.

>>> len(data)
4

Join two lists: To join two list we can use + operator.

>>> list1 = [1, 4, 6, "hello"]
>>> list2 = [2, 8, "bye"]
>>> list1 + list2
[1, 4, 6, 'hello', 2, 8, 'bye']

What happens if we try to access a element position which is not available in the list? We get a list index out of range error in such condition.

>>> list1[6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

Tuples:

The tuple is one of a data type which is ordered and immutable ( i.e. Data cannot be changed )

Let’s create a tuple:

>>> data = ( 1, 3 , 5, "bye")
>>> data
(1, 3, 5, 'bye')

Access tuple element: We can access elements in the tuple as same as we access in list:

>>> data[3]
'bye'

We can access the index range as follows:

>>> data[2:4]
(5, 'bye')

Change tuple value: If you are thinking how can we change the value of tuple then you are right my friend. We cannot change the value of tuple as it is immutable. We get the following error if we try to change the value of tuple:

>>> data[1] = 8
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

There’s a workaround available to change the value of tuple:

>>> data = ( 1, 3 , 5, "bye")
>>> data_two = list(data) # Convert data to list
>>> data_two[1] = 8 # Update value as list is mutable
>>> data = tuple(data_two) # Convert again to tuple
>>> data
(1, 8, 5, 'bye')

All other methods that we have seen in the list are applicable for the tuple also.

[ Note: Once a tuple is created a new value cannot be added in it. ].

Sets:

Sets is another data type in python which is unordered and unindexed. Sets are declared as follows:

>>> data = { "hello", "bye", 10, 15 }
>>> data
{10, 15, 'hello', 'bye'}

Access value: As sets are unindexed we cannot directly access the value in a set. Thus to access the value in the set you need to use a for loop.

>>> for i in data:
...     print(i)
...

10
15
hello
bye

Change Value: Once the set is created, values cannot be changed.

Add Item: To add an item to the set python provides an inbuilt method called add.

>>> data.add("test")
>>> data
{10, 'bye', 'hello', 15, 'test'}

Check length: To check the length of the set we use the len method.

>>> len(data)
5

Remove item: To remove an item use the remove method:

>>> data.remove("test")
>>> data
{10, 'bye', 'hello', 15}

Functions and Arguments:

Functions are a handy way to declare an operation that we want to perform. With the help of function, you can separate logic according to the operation.

They are a block of code that helps us in the reusability of the repetitive logic. Functions can be inbuilt and user-defined both.

To declare a function we use the def keyword. Following is the syntax of the functions:

>>> def hello_world():
...     print("Hello world")
...

Here we are declaring a function which when called prints a Hello world statement. To call a function we use the following syntax.

>>> hello_world()

We will get the following output:

Hello world

Remember () bracket in a function call means to execute it. Remove those round brackets and try the call again.

>>> hello_world

You’ll get the following output:

<function hello_world at 0x1083eb510>

When we remove the round bracket from the function call then it gives us a function reference. Here above as you can see the reference of function hello_world points to this memory address 0x1083eb510.

Consider you have to perform an addition operation. You can do it by declaring a and b then performing the addition.

>>> a = 5
>>> b = 10
>>> a + b
15

This is one way to go but now consider the value of a and b have changed and you need to do it again.

>>> a = 5
>>> b = 10
>>> a + b
15
>>> a = 2
>>> b = 11
>>> a + b
13

This still looks doable. Now imagine we need to add a set of two numbers a hundred times. The numbers within the set are different for every calculation. That’s a lot to do. Don’t worry we have a function at our disposal to solve this issue.

>>> def add(a,b):
...     print(a+b)
...

Here we are adding a and b as a compulsory argument to the add function. To call this function we will use the following syntax:

>>> add(10,5)

Output:

15

See how easy it is to define a function and use it. So what happens if we don’t pass an argument?

>>> add()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add() missing 2 required positional arguments: 'a' and 'b'

Python throws a TypeError and informs us that function requires two arguments.

Can you guess what will happen if we pass a third argument?

>>> add(10,5,1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add() takes 2 positional arguments but 3 were given

Well, python will inform us that we have pass 3 arguments but there are only 2 positional arguments.

So what can we do when we don’t know how many arguments a function can take? To solve this issue we use args and kwargs.

Args:

When you don’t know how many arguments will be passed to the function, args and kwargs are used in that scenario.

To pass n number of arguments to a function we use args. We add a * behind the argument.

Remember when you attach a * in front, you will be receiving a tuple of arguments.

>>> def add(*num):
...     print(num)
...

Here *num is an args. Now when we call the function add we can pass n number of arguments and it won’t throw a TypeError.

>>> add(1,2,3)
(1, 2, 3)

>>> add(1,2,3,4)
(1, 2, 3, 4)

Now to perform the addition operation we will use the Python’s builtin function sum

>>> def add(*num):
...     print(sum(num))
...

Now when we call the add function we will get the following output:

>>> add(1,2,3) # Function call
6
>>> add(1,2,3,4) # Function call
10

Keyword Arguments:

There is some condition where we don’t know the order of how arguments will be passed to the function when they are called. In such a scenario we use keyword arguments because you can pass them in any order in your call and our function will know the value. Let’s understand this concept by example.

>>> def user_details(username, age):
...     print("Username is", username)
...     print("Age is", age)
...

Let’s call this function as follow:

>>> user_details("Sharvin", 100)

We will get the following output:

Username is Sharvin
Age is 100

Well this looks correct but imagine if we called our function in this way:

>>> user_details(100, "Sharvin")

We will get the following output:

Username is 100
Age is Sharvin

This does not look right. What happened is username took value of 100 while age took value of Sharvin. In scenario’s like this where we don’t know the order of arguments we can use keyword arguments when calling the function as follows:

>>> user_details(age=100, username="Sharvin")

Output:

Username is Sharvin
Age is 100

See this is magic we got our output correct.

Default Argument:

Suppose there is a condition where we are not sure if a particular argument will get a value or not when the function is called. In such a scenario we can use Default arguments as follows:

>>> def user_details(username, age = None):
...     print("Username is", username)
...     print("Age is", age)
...

Here we are assigning a None to our age argument. If we don’t pass second argument while calling the function it will take None as a default value.

Let’s call the function:

>>> user_details("Sharvin")

Output:

Username is Sharvin
Age is None

Remember when we pass the second argument, in such a scenario it will override the None and use it as the value.

>>> user_details("Sharvin", 200)
Username is Sharvin
Age is 200

What will happen if assigned the first argument in our function as default and second as compulsory argument? Go to the python shell and try this out.

>>> def user_details(username=None, age):
...     print("Username is", username)
...     print("Age is", age)
...

You’ll get the following error:

  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument

Remember: All compulsory arguments must be declared first and then default argument must be declared.

kwargs:

There can be a situation where you don’t know how many keyword arguments will be passed into the function. In such a scenario we can use Kwargs.

To use kwargs we put ** in front of the argument.

Remember: When you attach a ** in front, you will be receiving a dictionary of arguments.

Let’s understand this by example. We will declare a function which accepts username as it’s argument with ** in front of it.

>>> def user(**username):
...     print(username)
...

When we call the user function as follows we will receive a dictionary.

>>> user(username1="xyz",username2="abc")

Output:

{'username1': 'xyz', 'username2': 'abc'}

So what’s happening here? It looks the same as Args, right? No, it’s not. In args, you cannot access a particular value by its name as it is in the form of a tuple. Here we get the data in the form of a dictionary. So we can easily access the value. Consider this example:

>>> def user(**user_details):
...     print(user_details['username'])
...

Let’s call our function:

>>> user(username="Sharvin",age="1000")

And you’ll get the following output:

Sharvin

Scope:

A scope defines where a variable or function is available. There are two types of scope in python i.e. Global and Local.

Global Scope: A variable or function created in the main body of python code is called a global variable or function and is part of the global scope. Let’s understand this concept by example:

>>> greet = "Hello world"
>>> def testing():
...     print(greet)
...
>>> testing()
Hello world

Here variable greet is available globally because it is declared in the body of python.

Local Scope: A variable or function created inside a function is called a local variable or function and is part of the local scope. Let’s understand this concept by example:

>>> def testing():
...     greet = "Hello world"
...     print(greet)
...
>>> testing()
Hello world

Here greet is created inside the testing function and is available there only. Let’s try to access it in our main body and see what happens.

>>> print(greet)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'greet' is not defined

Remember: To test for the above condition remember to restart the python console by clicking ctrl + d and again starting the python shell using python3 command. The reason behind this is that we have already declared greet variables in our global scope and it’s available in our memory.

As greet is not available globally we get the error that it is not defined.

Return Statement:

Till now our functions are pretty much dumb. They are receiving the data, processing it, and printing them. But in the real world, you need a function to give output. So that this output can be used in different operations.

To achieve this, return statements are used. Remember return statements are only part of functions and methods. The syntax for the return statement is quite easy.

>>> def add(a, b):
...     return a + b
...
>>> add(1,3)
4

Instead of printing our addition, we are returning the output. The value of the returned output can also be stored in a variable.

>>> sum = add(5,10)
>>> print(sum)
15

Lambda Expression:

Consider a situation where you don’t want to perform much computation in a function. In such a situation writing a full-fledged function doesn’t make sense. To solve this we use a lambda expression.

So what is a lambda function? It is an anonymous function and they are restricted to a single expression. The lambda function can take n number of arguments.

The syntax for lambda function is:

variable = lambda arguments: operation

Let’s understand it more by example:

>>> sum = lambda a: a + 10

Here we have declared a variable sum which we are using to call the lambda function. a represents the argument that is passed to that function.

Let’s call our function:

>>> x(5)
15

List comprehension:

Consider a situation where you want a list of squares. Normally you’ll declare a squares list and then in a for loop you’ll square out the numbers.

>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Well this is doable but we can achieve this in a single line with the help of list comprehension.

There are two ways to achieve this. Let’s understand both of them.

>>> squares = list(map(lambda x: x**2, range(10)))
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Here we are using list constructor to build a list and inside that lambda function which squares out the number. Another way to achieve the same result is as follows:

>>> squares = list(x**2 for x in range(10))
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

I prefer this way because it is easier to understand because of its readability and conciseness.

What about when we have a condition where we want a set of two numbers that are the same. Well, we need to write two for loops and one if loop.

Let’s see how that will look like:

>>> num_list = []
>>> for i in range(10):
...     for j in range(10):
...             if i == j:
...                     num_list.append((i,j))
...
>>> num_list
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]

That’s a lot of work and in terms of readability, it’s hard to understand.

Let’s use list comprehension to achieve the same result.

>>> num_list = list((i,j) for i in range(10) for j in range(10) if i == j)

>>> num_list
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]

See How easy it is to get the same output in a single expression. Well, that’s the power of list comprehension. As you start developing an application you’ll find out that list comprehension is powerful.

OOPS concepts:

Python is a multi-paradigm programming language. It means python can use different approaches to solve the problem. One of the paradigms is procedural or functional programming. It structures the code like a recipe; set of steps in the form of function and code block.

Another approach to solving the problem is by creating classes and objects. This is known as object-oriented oriented programming. An object is a collection of data (variables) and methods that act on those data. And, the class is a blueprint for the object.

The important part to understand in object-oriented programming is that objects are at the center of the paradigm, not only representing the data but it also represents the structure of the program.

You can choose the paradigm that best suits the problem at hand, mix different paradigms in one program, and/or switch from one paradigm to another as your program evolves.

Advantages of object oriented programming:

  • Inheritance: This is one of the most useful concepts in oops. It specifies that the child object will have all the properties and behavior of the parent object. Thus Inheritance allows us to define a class that inherits all the methods and properties from another class.
  • Polymorphism: To understand polymorphism let’s divide the word into two parts. The first part poly means many and morph means to form or shape. Thus in simple one-word polymorphism means one task can be performed in many different ways. For example, you have a class animal, and all animals speak. But they speak differently. Here, the “speak” behavior is polymorphic and depends on the animal. So, the abstract “animal” concept does not actually “speak”, but specific animals (like dogs and cats) have a concrete implementation of the action “speak”. Polymorphism means the same function name or method name being used for different types.
  • Encapsulation: In object-oriented programming you can restrict access to methods and variables; we can make the methods and variables private. This can prevent the data from being modified by accident and is known as encapsulation.

First, we will understand classes, objects, constructor and after that, we will look into the above properties again. If you know about the classes, object, and constructor you can skip to the section that you want to read.

Classes:

There are primitive data structures available in python for example numbers, strings, and lists that can be used for simple representation like name, place or cost, etc.

But what if we have a condition where we have complex data. There is a pattern in the repetition of the properties of that data in that scenario what can we do?

Suppose we have a data of 100 different animals. Every animal has a name, age, legs, etc. What if we want to add other properties to animal or one more animal gets added to that list. To manage all such complex scenario’s we need classes.

According to official python documentation, Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made.

Each class instance can have attributes attached to it for maintaining its state. Class instances can also have methods (defined by its class) for modifying its state.

Syntax of class:

class ClassName:

    <expression-1>
    .
    .
    .
    <expression-N>

We use class keyword to define a class. We will define a class Car.

class Car:
    pass

Methods:

Methods look the same like function only difference is that methods are dependent on the object. A function can be invoked by name while methods need to be invoked by using their class reference. They are defined inside the class.

In our example, let’s create 2 methods. One is an engine and another is a wheel. These 2 methods define the parts available in our car.

The below program will give us a better idea of classes:

>>> class Car:
...     def engine(self):
...             print("Engine")
...

>>> Car().engine()
Engine

Here we are calling the engine method by using the Car() reference.

To summarise, the class provides a blueprint of what should be defined but it does not provide any real content. The Car class above defines the engine but it will not state what a specific car’s engine is. It is specified by the object.

Objects:

If you are fully clear with classes and method we will move to objects. If you still have any doubt reread both the sections again.

The object is an instance of the class. Let’s consider the above example of a car. Here Car is our class and toyota is the object of the car. We can create multiple copies of the object. Every object must be defined using the class.

The syntax for creating an object is:

toyota = Car()

Let’s consider our Car example for understanding the objects more better.

class Car:

    def engine(self):
        print("Engine")

    def wheel(self):
        print("Wheel")

toyota = Car()

The above toyota = Car() is a class object. Class objects support two kinds of operations: attribute references and instantiation.

Class instantiation uses function notation. The instantiation operation (“calling” a class object) creates an empty object.

Now we can call different methods from our class Car using the object toyota that we have created. let’s call the method engine and wheel.

Open your editor and create a file named mycar.py. In that file copy the code below:

class Car:

    def engine(self):
        print("Engine")

    def wheel(self):
        print("Wheel")

if __name__ == "__main__":
    toyota = Car()
    toyota.engine()
    toyota.wheel()

Save the above code. Let’s under our program.

Here we are creating a toyota object with the help of Car class. The toyota.engine() is a method object. What exactly happens when a method object is called? In the call toyota.engine() doesn’t take any argument but if you see the method declaration we can see that it takes a self argument. You may be confused about why it is not throwing an error. Well whenever we use a method object, the call toyota.engine() is converted to Car.engine(toyota). We will understand more about the self in the upcoming section.

Run the program using the following command.

python mycar.py

You’ll get the following output:

Engine
Wheel

Constructor:

The __init__ method is the constructor method in python. The constructor method is used to initialize the data.

Go to the python shell and test the below example:

>>> class Car():
...     def __init__(self):
...             print("Hello I am the constructor method.")
...

When we will call our class we will get the following output:

>>> toyota = Car()
Hello I am the constructor method.

Note: You will never have to call the init() method; it gets called automatically when you create a class instance.

Instance attributes:

All the classes have objects and all the objects have attributes. Attributes are the properties. We use __init__() method to specify an object’s initial attribute.

Let’s consider our car example:

class Car():
    def __init__(self, model): 
        self.model = model  #instance attribute

In our example, each Car() has a specific model. Thus instance attributes are unique data to each instance.

Class attributes:

We saw that instance attributes are specific to each object but class attributes are the same for all the instances. Let us look at the example of the car with the help of class attributes.

class Car():

    no_of_wheels = 4 #class attribute

So each car can have different models but all the cars will have 4 wheels only.

Self:

Now let’s understand what does self means and how we use it in object-oriented programming. The self represents the instance of a class. Using the self keyword we can access the data initialize in the constructor and methods of a class.

Let us look at an example of how the self can be used. Let’s create a method named brand under our class Car.

Inside that __init__ method, we will pass a model by passing our car’s model name when we are instantiating our object. This name can be accessed anywhere in the class for example self.model in our case.

Go to the file named mycar.py and replace old code with this code:

class Car(): 

  def __init__(self, model): 
    self.model = model

  def brand(self): 
    print("The brand is", self.model)  

if __name__ == "__main__":
  car = Car("Bmw")
  car.brand()

Now when we run our above program using the following command:

python mycar.py

We will get the following output:

The brand is Bmw

Note:self is a convention and not a real python keyword. A self is a argument in method and we can use another name in place of it. But it is recommended to use self because it increases the readability of code.

Inheritance:

When a class inherits the property of another class then this scenario is called inheritance.

The class from which properties are inherited is called the base class. The class which inherits the property of another class is called the derived class.

Inheritance can be defined as a parent and child relationship. The child inherits the properties of the parent. Thus making the child a derived class while parent as a base class. Here term property refers to attributes and methods.

The syntax for a derived class definition looks like this:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

It’s important to note that child classes override or extend the attributes and behaviors of parent class methods. In other words, child classes inherit all of the parent’s attributes and behaviors but can also specify different behavior to follow.

The most basic type of class is an object, which generally all other classes inherit as their parent. Let’s modify our previous example to understand how inheritance works.

We will create a base class named vehicle:

class Vehicle:
    def __init__(self, name):
        self.name = name

    def getName(self):
        return self.name

We have created a class Vehicle and instantiated a constructor with self.name which we are using in getName method. Whenever this method will be called, it will return the name that has been passed when an object is instantiated for that class.

Now let’s create a child class Car:

class Vehicle:
    def __init__(self, name):
        self.name = name

    def getName(self):
        return self.name

class Car(Vehicle):
  pass

Car is a child class of Vehicle. It inherits all the method and attributes of parent class.

Now let’s use methods and attribute from the Vehicle class in our child class Car.

class Vehicle:

    def __init__(self, name, color='silver'):
        self.name = name
        self.color = color

    def get_name(self):
        return self.name

    def get_color(self):
        return self.color

class Car(Vehicle):
  pass

audi = Car("Audi r8")
print("The name of our car is", audi.get_name(), "and color is", audi.get_color())

Let’s understand what we have done here. We have declared a class named Vehicle with a constructor that takes name as an argument while color has default argument. We have two methods inside it. The get_name returns name while the get_color returns the color. We have instantiated a object and passed the car name.

One thing you’ll notice here that we are using base class methods in our child class declaration.

Run the above program using the following command:

python mycar.py

Output:

The name of our car is Audi r8 and color is silver

We can also override a parent method or attribute. In the above example, we have defined our vehicle color has silver. But what if the color of our car is black?

Now for every child class, we can’t make changes in the parent class. There comes the overriding functionality.

class Vehicle:

    def __init__(self, name, color='silver'):
        self.name = name
        self.color = color

    def get_name(self):
        return self.name

    def get_color(self):
        return self.color

class Car(Vehicle):

    def get_color(self):
        self.color = 'black'
        return self.color

audi = Car("Audi r8")
print("The name of our car is", audi.get_name(), "and color is", audi.get_color()

As you can see in the above program, I have not instantiated a constructor. The reason behind this is that our child class Car is only using attributes from the Vehicle class and it is already inheriting them. So in such a scenario, there is no need to re-instantiate these attributes.

Now When we run the above program we will get the following output:

The name of our car is Audi r8 and color is black

Super:

The super() returns a temporary object of the superclass that then allows us to call that superclass’s methods. Calling the previously built methods with super() saves us from needing to rewrite those methods in our subclass, and allows us to swap out superclasses with minimal code changes. Thus super extends the functionality of the inherited method.

Let’s extend our car example using super(). We will instantiate a constructor with brand_name and color in the parent class; Vehicle. Now we will call this constructor from our child class (Car) using super. We will create a get_description method which is returning self.model from Car class and self.brand_name, self.color from the Vehicle class.

class Vehicle:

    def __init__(self, brand_name, color):
        self.brand_name = brand_name
        self.color = color

    def get_brand_name(self):
        return self.brand_name

class Car(Vehicle):

    def __init__(self, brand_name, model, color):  
        super().__init__(brand_name, color)       
        self.model = model

    def get_description(self):
        return "Car Name: " + self.get_brand_name() + self.model + " Color:" + self.color

c = Car("Audi ",  "r8", " Red")
print("Car description:", c.get_description())
print("Brand name:", c.get_brand_name())

When we run the above program we get following output:

Car description: Car Name: Audi r8 Color: Red
Brand name: Audi

Multiple Inheritance:

When a class inherits the method and attributes from multiple parent class then it is called multiple inheritance. This allows us to use the property from multiple base classes or parent classes in a derived or child class.

The general syntax of Multiple Inheritance is as follows:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

Let’s extend our vehicle example using multiple inheritance property. Here in this example, we will create 3 classes i.e. Vehicle, Cost and Car

Class Vehicle and Cost will be the Parent class. A Vehicle class represents the general property while the Cost class represents its pricing.

As the Car has a general property and costing both it will have 2 parent classes. Thus we will inherit multiple parent classes.

class Vehicle:

    def __init__(self, brand_name):
        self.brand_name = brand_name

    def get_brand_name(self):
        return self.brand_name

class Cost:		

    def __init__(self, cost):
        self.cost = cost

    def get_cost(self):
        return self.cost

class Car(Vehicle, Cost):	

    def __init__(self, brand_name, model, cost): 
        self.model = model 
        Vehicle.__init__(self, brand_name) 
        Cost.__init__(self, cost) 

    def get_description(self):
        return self.get_brand_name() + self.model + " is the car " + "and it's cost is " + self.get_cost()

c = Car("Audi ",  "r8", "2 cr")
print("Car description:", c.get_description())

Here you will find one difference in the above program than other programs from above. I have used Vehicle.__init__(self, brand_name) in the constructor of Car class. This is one way of calling attributes from the parent class. Another being super which I have explained above. When we run the above program we will get the following output:

Car description: Audi r8 is the car and it's cost is 2 cr

Though it can be used effectively, multiple inheritance should be done with care so that our programs do not become ambiguous and difficult for other programmers to understand.

Polymorphism:

The word polymorphism means having many forms. In programming, polymorphism means same function name (but different signatures) being uses for different types.

Let’s extend our car program using polymorphism. We will create 2 classes Car and Bike. Both the classes have common method or function but they are printing different data. The program is pretty much self-explanatory.

class Car: 

    def company(self): 
        print("Car belongs to Audi company.")

    def model(self): 
        print("The Model is R8.") 

    def color(self): 
        print("The color is silver.") 

class Bike: 

    def company(self): 
        print("Bike belongs to pulsar company.") 

    def model(self): 
        print("The Model is dominar.") 

    def color(self): 
        print("The color is black.") 

def func(obj): 
    obj.company() 
    obj.model() 
    obj.color() 

car = Car() 
bike = Bike() 

func(car) 
func(bike)

When we run the above code we will get the following output:

Car belongs to Audi company.
The Model is R8.
The color is silver.
Bike belongs to pulsar company.
The Model is dominar.
The color is black.

Encapsulation:

In most of the object-oriented programming, we can restrict access to methods and variables. This can prevent the data from being modified by accident and is known as encapsulation. This is also available in python.

Let’s integrate encapsulation into our car example. Now consider we have a super-secret engine. In the first example, we will hide our engine using a private variable. In the second example, we will hide our engine using a private method.

Example 1:

class Car:

  def __init__(self): 
    self.brand_name = 'Audi '
    self.model = 'r8'
    self.__engine = '5.2 L V10'

  def get_description(self):
        return self.brand_name + self.model + " is the car"

c = Car()
print(c.get_description)
print(c.__engine)

In this example self.__engine is a private attribute. When we run this program we will get the following output.

Audi r8 is the car
AttributeError: 'Car' object has no attribute '__engine'

We get an error that Car object doesn’t have _engine because it is a private object.

Example 2:

We can also define a private method by adding __ in front of the method name. Following is the example of how we can define a private method.

class Car:

  def __init__(self):
      self.brand_name = 'Audi '
      self.model = 'r8'

  def __engine(self):
      return '5.2 L V10'

  def get_description(self):
      return self.brand_name + self.model + " is the car"

c = Car()
print(c.get_description())
print(c.__engine()) 

In this example def __engine(self) is a private method. When we run this program we will get the following output.

Audi r8 is the car
AttributeError: 'Car' object has no attribute '__engine'

Now suppose we want to access the private attribute or method we can do it in the following way:

class Car:

  def __init__(self):
      self.brand_name = 'Audi '
      self.model = 'r8'
      self.__engine_name = '5.2 L V10'

  def __engine(self):
      return '5.2 L V10'

  def get_description(self):
      return self.brand_name + self.model + " is the car"

c = Car()
print(c.get_description())
print("Accessing Private Method: ", c._Car__engine()) 
print("Accessing Private variable: ", c._Car__engine_name)

The output of following program is:

Audi r8 is the car
Accessing Private Method:  5.2 L V10
Accessing Private variable:  5.2 L V10

Encapsulation gives you more control over the degree of coupling in your code, it allows a class to change its implementation without affecting other parts of the code.

Decorator:

Imagine you have to extend the functionality of multiple functions. How will you do that? Well, one way is you can make functional calls and in that function, you can handle it. Making changes in 30 to 40 function calls and remembering where to place the call is a messy task. But there is a more elegant way provided by python and that is Decorator.

What is a decorator? A decorator is a function that takes a function and extends its functionality without modifying explicitly. Well, I understand if you are still confused about what decorators are. Don’t worry we have a tool named example to explain it.

Let’s try an example to understand the decorator. There are 2 ways to write a decorator.

Way 1: We declare a decorator function and in the arguments of the function we expect the function to be passed as an argument. Inside that, we write a wrapper function where operations are carried out and it is returned.

>>> def my_decorator(func):
...     def wrapper():
...             print("Line Number 1")
...             func()
...             print("Line Number 3")
...     return wrapper
...
>>> def say_hello():
...     print("Hello I am line Number 2")
...

To call the function we assign the decorator with say_hello as an argument.

>>> say_hello = my_decorator(say_hello)

We can also check the reference using say_hello. We will get the output that informs us it has been wrapped by the my_decorator function.

<function my_decorator.<locals>.wrapper at 0x10dc84598>

Let’s call our say_hello function:

>>> say_hello()
Line Number 1
Hello I am line Number 2
Line Number 3

See the magic the line “Hello I am line Number 2” gets printed in between Line Number 1 and 3 because the function call get’s executed there.

The way no 1 is clunky and because of that many people prefer way no 2. Let’s understand it.

Way No 2: Here our decorator declaration remains same but we change how the call is assign to that decorator. Whichever function require that decorator wraps it self with @decorator_name.

>>> def my_decorator(func):
...     def wrapper():
...             print("Line Number 1")
...             func()
...             print("Line Number 3")
...     return wrapper
...
>>> @my_decorator
... def say_hello():
...     print("Hello I am line Number 2")
...
>>> say_hello()

Output is the same:

Line Number 1
Hello I am line Number 2
Line Number 3

A decorator is a powerful tool and it is used in the following development scenario of an application:

  • Setup logger
  • Setup configuration
  • Setup Error catching
  • Extending common functionality for all function and classes

Exceptions:

When we were learning various syntax we came around various errors. Those were the error because of the syntax. But in a real-world application, errors or commonly known as a bug not only occur due to the syntax issue but also because of network error or some other cause.

To handle these issues we use Try - Except. In try block, we write the expression that we want to be executed while in except block we catch the error. Try-Except block looks as follows:

try:
	expression
except:
	catch error

Let’s understand this by an example:

>>> try:
...     print(value)
... except:
...     print("Something went wrong")
...

Here we are trying to print value variable but it is not defined. So we get the following output:

Something went wrong

You may be wondering the line “something went wrong” is not that helpful. So how can I know what went wrong here?

We can the exception and use it to find out what went wrong. Let’s test this in our example:

>>> try:
...     print(value)
... except Exception as e:
...     print(e)
...

And the result is:

name 'value' is not defined

Wooh!! that’s magic. It is notifying me that ‘value’ is not defined.

Python also provides a tool named raise. Suppose you don’t want certain condition to occur and if it occurs you want to raise it. In such condition you can use it. Consider the example above:

>>> i = 5
>>> if i < 6:
...     raise Exception("Number below 6 are not allowed")
...

The output we get is as follows:

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: Number below 6 are not allowed

There are many sub type of Exception and I would recommend you to go through Python Documentation to understand them.

Package Import:

You have learned python and now you are all ready to build awesome applications. But before that wait for my friend. We are still missing some important topics.

Without package import, you will be forced to write everything in one single file. Imagine what a mess it will be.

Create two files named main.py and hello.py. Remember both file needs to be in same directory.

Under hello.py copy paste the following code:

def say_hello():
    print("Hello world")

Under main.py copy paste the following code:

import hello

if __name__ == "__main__":
    hello.say_hello()

In hello.py we have declared a say_hello() function which prints “Hello world”. In main.py you’ll see a import statement. We are importing the hello module and calling say_hello() function from that module.

Run our program using the following command:

➜ python main.py

Output:

Hello world

Now let’s understand how to import a module which is in another directory.

Let’s create a directory named data and move our hello.py inside that directory.

Go to the main.py and change the previous import statement.

from data import hello

if __name__ == "__main__":
    hello.say_hello()

There are two ways to import from a directory.

  • Way1: from data import hello
  • Way2: import data.hello

I prefer way1 because of its readability. You can choose any way which you like.

Let’s run our application using the following command:

➜ python main.py

And error occurs. Wait why did this happened? We did everything right. Let’s see go through the error:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from data import hello
ImportError: No module named data

Well python is telling us that it doesn’t know a module named data. To solve this issue create a __init__.py inside data directory. Leave the file blank and run the program again and you’ll get the following output:

Hello world

Well python by default does not treat a directory as module. To inform python to treat a directory as module, __init__.py is required.

JSON Handling:

If you worked previously with web development or app development you may be aware that all the API calls take place in JSON format. JSON looks similar to a dictionary in python. Remember JSON is not a dictionary.

To handle the JSON python provides a builtin json package. To use this package we need to import it as follows:

import json

This library provides two methods which help us in handling the JSON. Let’s understand them one by one.

JSON loads:

If you have JSON string and want to convert it back to the dictionary you need to use a loads method. Let’s understand them by example. Go to the python shell and copy-paste the following code:

>>> import json
>>> json_string = '{ "user_name":"Sharvin", "age":1000}' #JSON String
>>> type(json_string)
<class 'str'>
>>> data = json.loads(json_string)
>>> type(data)
<class 'dict'>
>>> data
{'user_name': 'Sharvin', 'age': 1000}

JSON dumps:

Now let’s convert our data back to the JSON string format using the dumps method.

>>> jsonString = json.dumps(data)
>>> type(jsonString)
<class 'str'>
>>> jsonString
'{"user_name": "Sharvin", "age": 1000}'

To know more about JSON Manipulation go through the Python’s Documentation.

Originally published by Sharvin Shah at https://www.freecodecamp.org

#python #web-development #machine-learning #data-science

Learn Python Programming From Beginner to Pro
18.70 GEEK