Broadly, there are two kinds of importing:
- Import declarations, which make other modules or their exported members available by name in the current scope;
- Import statements, which execute the code of another module, including its top-level declarations and any top-level statements it might have, and then bind that module or its exported members to some name or names in the current scope.
The distinction is that import statements may have side-effects, whereas declarations do not. Referring to your examples, Java and Rust have import declarations whereas Python has import statements.
There are other ways of handling modules, particularly "includes" (which dump the source code from the included file directly or somewhat directly into the current one), but I won't address those in this answer as it's already quite long. These are normally not called "imports".
Import declarations
Import declarations tend to be used in compiled languages, especially languages where statements cannot occur at the top-level of a compilation unit. These are generally the languages where you declare a "main" function as the program's entry point.
They have no side-effects because the modules they import don't have statements at the top-level, so there is nothing to execute just from "importing" those modules. The order of import declarations typically doesn't matter, and they are only used at compile-time; they have no effect at runtime, after the various modules have already been compiled and linked together into a single binary.
Import declarations may be implemented in a compiler by doing two passes over each module ─ one to find the names and types/signatures of all exported members of each module, and then another to resolve the names used in the imports of each module.
Import statements
Import statements tend to be used in interpreted, dynamic languages, especially languages where declarations of functions, classes, etc. are also statements. These are generally the languages where a program's entry point is just the top of the source file that the interpreter is executing.
They can have side-effects because the modules they import can have statements at the top-level, so the order of import statements can matter. Import statements in dynamic languages generally load the imported modules from the filesystem (or otherwise) at runtime, unless the module has already been loaded and cached by an earlier import of the same module. This is why they only appear in interpreted languages ─ in a compiled and linked binary, the source files (and hence, the separate modules) simply wouldn't exist at runtime.
Import statements will typically be implemented by an interpreter loading, parsing and evaluating the imported source file at runtime, basically invoking the interpreter itself recursively. Once the recursive invocation of the interpreter completes, the exported members may be bound as properties of an object representing the module, and then that object is bound to the name associated with the module, e.g. as in a simple import statement like import foo. Alternatively, an import statement like from foo import bar, baz may destructure this object in order to bind the exported members to simple names.
The loaded modules (and their exported members) are typically also cached, so that if they are imported multiple times, their side-effects only trigger once, and later imports don't have to parse and evaluate the whole source file each time. Additionally, an "empty" or "busy" placeholder may be added to the cache for a module while it is currently being loaded, in order to detect circular imports, which could otherwise cause unbounded recursion. If a circular import is detected, this may either be handled by throwing a runtime error, or just using the "empty" object in the cache instead of the unavailable result.