We use the import statement  when we want to use code from another python file(module) or library.

The import keyword  is the primary tool used to create import statements. 

Basically, an import statement has the following syntax:

import <module>

We simply use the import keyword followed by the name of the module we want to import.

import standard library module

import time #import a module called 'time'

current_time = time.ctime()
print(current_time)

In the above example, we imported a module called 'time'. This module already exists ready for use in Python's standard library. We then used a function called ctime() from the time module.

The import process

Several things happens when we use an import statement, they are as outlined below:

  1. The import statement searches for the module specified in the statement.
  2. If the module is found.
    • its code is executed.
    • The imported module is added to the current namespace, allowing its contents to be accessed using the dot notation (e.g. module.function() to access a function defined in the module)
  3. If the module is not found, a ModuleNotFoundError exception is raised.

Note that when the module is imported, its code is actually executed, this means that if there are any statements, variable declarations or function calls at the top level of the module, they will be executed once the module is imported, and any resulting changes or actions will take place immediately. Consider the following example:

foo.py 

#foo.py
#this module will be imported

def add(a, b):
    print(f'{a} + {b} = {a + b}')

a = 10
b = 20

add(a, b)
#main.py

import foo #imports the foo module

10 + 20 = 30

In the above example, we defined a module called foo.py which is imported by main.py.

Note that the output from the function call in foo.py is automatically seen when the module is imported in main.py. This should make you define and use modules with caution, for example, having unnecessary statements and function calls at the top-level of a module means that they will be propagated where the module is imported.

Advanced Import statements

The import statement is is more flexible than for simply importing a single module by its name. In this section we will see how we can use the import statement more flexibly to control how imports works.

Import multiple modules at once

We can import multiple module in the same import statement by separating their names with comas. The syntax is as shown below:

import <module1>, <module2>, <module3>, ...
#import multiple modules
import time, random, math 

#Use the modules
current_time = time.ctime()
random_number = random.randint(0, 100)
sqrt_of_16 = math.sqrt(16)

print(current_time)
print(random_number)
print(sqrt_of_16)

In the above example, we imported three built-in modules in the same import statement,  time, random and math. You can import as many modules as necessary by separating their names with  commas.

Importing a module under a different names

Sometimes we may want to import a module such that we can access it with an alias name instead of its actual name. This may be the case for example if : 

  1. There are multiple modules with similar names which can lead to conflicts.
  2. We want to use a more convenient name, for example if the name is too long, we may want to use a shorter more convenient name..
  3. We want to use a more intuitive or descriptive name for the module.
  4. We want to rename the module for organizational or stylistic reasons

To import a module under a different name, we use the import statement with an as clause. The basic syntax is as shown below:

import <module> as <new_name>
import datetime as dt

current_date = dt.date.today()
print(current_date)

In the above example, we imported the built-in datetime module  with an alias name, ''dt". This way, the module becomes accessible under the alias name.

Importing specific objects from a module

Note that in all the previous examples, we imported an entire module but ended up using just one or two functions from the module.

In case when we know that we are simply going to use only specific objects(functions, classes, etc ) from a module, we can use the import statement with a from clause to specify only those objects that we want to import. The syntax is as shown below:

from <module> import <object1>, <object2>, <object3>....

Only the specified  objects will be imported. An object refers to a function, a class, a variable, e.t.c

from math import sqrt, sin, log

#use the imported functions
print(sqrt(25))
print(sin(30))
print(log(1000))

In the above example, only sqrt, sin and log functions are imported from the builtin math module.

If necessary, you can alias the imported object using the as clause so that it will be available with the alias name instead of its actual name.

from math import sqrt as square_root, sin, log

#use the imported functions
print(square_root(25))
print(sin(30))
print(log(1000))

Import Everything

In case where you want to import all objects from a module, you can use the import statement with an asterisk(*), the syntax is as shown below:

from <module> import *

Note that this will make any object in the specified <module> get imported and  be available in the program's namespace. 

While this syntax may seem convenient, it should be used with caution and only when necessary or you know exactly what you are doing. This is because it may lead to unnecessary complexity as you may not end up using all the imported objects, as well as potential name conflicts.

from math import *

#everithing from math is now imported
print(sqrt(25))
print(log(100))
print(lcm(3, 4, 5))
print(gcd(30, 25, 15))

As you can see, in  the above example, after using the from math import * , all functions defined in the math module gets imported and added to the program's namespace.

The ModuleNotFoundError Exception

As earlier mentioned, whenever import statement fails to locate the module with the specified name, a ModuleNotrFoundError exception is raised.

This exception is especially common when we misspell the name of the module we want to import

import dattime

In the above example, we were intending to import the builtin module called datetime but we deliberately misspelled its name leading to the ModuleNotFoundError exception being raised. In practical scenarios, you will not misspell the names intentionally.

In cases where we anticipate that the ModuleNotFoundError may be raised, we can wrap the import statement in a try block and place the corrective action in the except block. For example, this may be the case when we are not sure whether a  third-party module is currently installed in the system.

try:
    import matplotlib
except ModuleNotFoundError:
    print('matplotlib is not installed. Please install matplotlib.')

Circular Imports

One of the most dreaded exceptions is the one that occurs due to circular imports.

A circular import happens when two or more modules are trying to import each other, creating a never-ending import loop. This can happen when there is a dependency between the modules. For example consider the following two modules:

#foo.py 

import spam
#spam.py 
import foo

Trying to use an object from a module when there is circular import will lead to an ImportError exception.

When  code in foo.py tries to access an object from spam.py, Python will try to import spam.py first. But since spam.py also imports objects from foo.py, it will again try to import foo.py, causing a never-ending loop. 

Good practices with imports

  • Conventionally, all import statements should be placed at the top of the file before any other code. This allows  for easier visibility and organization of imported modules.
  • Only import what you need, and generally avoid using  imports with asterisk(*) syntax. This helps reduce memory usage and potential name conflicts.
  • When using aliasing with the as clause, ensure that the alias names are meaningful and do not conflict with those of existing objects. 
  • Avoid circular imports.