Skip to content

Python vs. JavaScript: A Guide to Modules and Packages

Modularity is the cornerstone of building maintainable and scalable applications. It allows developers to split code into logical units for easy reuse and management. This document provides an in-depth comparison of the core concepts, syntax, and design philosophies of modularity and package management in Python and JavaScript.

References:


Core Concepts and Terminology Comparison

ConceptPythonJavaScript
Basic UnitModule - a single .py fileModule - a single .js file
OrganizationPackage - a directory containing __init__.pyPackage / Library - a directory defined by package.json
Package Identifier/Entry Point__init__.py (controls package behavior)package.json (defines metadata) and index.js (conventional entry point)
Import Keywordimport, from ... importimport, from ... import (ESM) / require (CommonJS)
Ecosystem & Toolspip & PyPI (Python Package Index)npm / yarn & npm registry
Run as Script Checkif __name__ == "__main__":require.main === module (Node.js)

1. Modules - The Basic Unit of Code Organization

In both languages, a module is the basic unit of code reuse, typically corresponding to a single file.

Python: Modules

In Python, any .py file can be treated as a module.

  • Implicit Exports: Python's export mechanism is implicit. All top-level variables, functions, and classes defined in a module file (e.g., my_module.py) automatically become the public API of that module and can be imported by other files.

    python
    # my_module.py
    PI = 3.14
    def calculate_area(radius):
        return PI * radius ** 2
  • Importing (import): Python provides flexible import syntax.

    python
    # main.py
    # Method 1: Import the entire module
    import my_module
    print(my_module.PI)
    print(my_module.calculate_area(10))
    
    # Method 2: Import specific members from the module
    from my_module import calculate_area, PI
    print(PI)
    print(calculate_area(10))
    
    # Method 3: Use an alias
    from my_module import calculate_area as area
    print(area(10))
    
    # Method 4: Import all (not recommended)
    # from my_module import *
    # print(PI) # Can lead to namespace pollution

JavaScript: Modules (ESM)

JavaScript has evolved from community specifications (CommonJS) to a language standard (ESM - ES Modules). We will focus on the modern ESM standard.

  • Explicit Exports: In contrast to Python, JavaScript module exports must be explicit, using the export keyword.

    javascript
    // my-module.js
    // Named Exports
    export const PI = 3.14;
    export function calculateArea(radius) {
      return PI * radius * radius;
    }
    
    // Default Export - only one per module
    export default function sayHello() {
      console.log("Hello from module!");
    }
  • Importing (import):

    javascript
    // main.js
    // Import named exports (names must match)
    import { PI, calculateArea } from './my-module.js';
    console.log(PI);
    console.log(calculateArea(10));
    
    // Import the default export (can be named arbitrarily)
    import greetingFunction from './my-module.js';
    greetingFunction(); // "Hello from module!"
    
    // Import a named member with an alias
    import { calculateArea as area } from './my-module.js';
    console.log(area(10));

Core Comparison

  1. Explicit vs. Implicit Exports: Python exports all top-level objects in a file by default, whereas JS's ESM requires an explicit export for what to expose. This makes the public API of JS modules more clearly defined.
  2. Default Exports: JS has a clear concept of export default, allowing a module to export a primary "value," which is also more convenient to import. Python has no direct equivalent, though a package's __init__.py can simulate a similar effect.

2. Packages - Directories for Organizing Modules

As projects grow, related modules need to be organized into directories, which are known as packages.

Python: Packages

A Python package is essentially a directory that contains a special file, __init__.py.

  • The Role of __init__.py:

    1. Identifier: Its presence tells the Python interpreter that the directory should be treated as a package.
    2. Initialization: You can execute package-level initialization code in this file.
    3. API Facade: You can import members from submodules within __init__.py to "promote" them to the package's top-level namespace, making them easier to call from the outside.
    4. Control Imports: Using the __all__ list, you can precisely define which modules or variables should be imported when from my_package import * is used.
  • Example Structure:

    my_app/
    └── my_package/
        ├── __init__.py
        ├── math_ops.py
        └── string_ops.py
    python
    # my_package/math_ops.py
    def add(a, b): return a + b
    
    # my_package/__init__.py
    from .math_ops import add # Promote the add function to the my_package level
    print("my_package is being initialized!")
    
    # main.py
    from my_package import add
    print(add(2, 3)) # Import add directly from my_package

JavaScript: Packages

In the JavaScript ecosystem, a "package" usually refers to a unit of code that can be managed and distributed via npm. Its core is the package.json file.

  • The Role of package.json:

    1. Metadata: Contains information like the package's name, version, description, and author.
    2. Dependency Management: Defines the libraries required for the project to run and for development.
    3. Entry Point: Tells Node.js or a bundler which file to load when the package is imported, via the "main" or "module" fields.
  • The index.js Convention: Similar to Python's __init__.py, index.js is a community convention that usually serves as the default entry point for a package, used to organize and export the package's public API.

  • Example Structure:

    my-package/
    ├── package.json
    ├── index.js
    └── lib/
        ├── mathOps.js
        └── stringOps.js
    json
    // package.json
    {
      "name": "my-package",
      "version": "1.0.0",
      "main": "index.js"
    }
    javascript
    // index.js
    import { add } from './lib/mathOps.js';
    export { add }; // Re-export 'add' as the public API of the package

Core Comparison

  • Driving Mechanism: Python's package system is driven by the language interpreter and the __init__.py file. JS's package system is driven by an external toolchain (npm) and the package.json configuration file, focusing more on metadata management and dependency resolution.
  • Ecosystem: Both have powerful central repositories (PyPI and npm registry), but the JS npm ecosystem is larger and more complex in terms of scale, toolchain integration, and client-side development.

3. Unifying Executable Scripts and Importable Modules

Python: if __name__ == "__main__"

This is a classic Python idiom. __name__ is a built-in variable that is set to "__main__" when a .py file is executed directly; when it is imported as a module, its value is the module's name. This allows a file to serve as both an executable script and an importable library.

python
# my_script.py
def main_function():
    print("This is a reusable function.")

if __name__ == "__main__":
    print("Script is being run directly.")
    main_function()

JavaScript (Node.js): require.main === module

In Node.js's CommonJS module system, a similar mechanism exists to determine if a file is the main entry point.

javascript
// my-script.js
function mainFunction() {
  console.log("This is a reusable function.");
}

if (require.main === module) {
  console.log("Script is being run directly.");
  mainFunction();
}

For ES Modules, this check is slightly more complex, often requiring a comparison of import.meta.url and process.argv. Python's __name__ mechanism is simpler and more widely known.


4. A Deeper Look at Python's Package Import Mechanism

To fully understand Python's modularity, it's essential to understand its package import mechanism, which is driven by the interpreter and a specific file structure.

References: The import system - Python 3 Docs

Two Types of Packages

Python defines two types of packages:

  1. Regular Packages: This is the traditional and most common type. A regular package is a directory containing an __init__.py file. The presence of this file explicitly tells the Python interpreter that the directory is a package.
  2. Namespace Packages: This is a more advanced mechanism introduced in Python 3.3+. It allows a single logical package to be split across multiple physical directories. These directories share the same package name but do not contain an __init__.py file. When imported, Python aggregates the contents of all same-named directories into a single virtual namespace. Unless you are building a complex, splittable library, you will most often be working with regular packages.

Regular Package Import Process

When the Python interpreter executes an import statement, it follows a clear process:

  1. Search Path (sys.path): The interpreter iterates through all paths in the sys.path list to find a matching module file or package directory.

  2. Module Cache (sys.modules): Before searching, the interpreter first checks the sys.modules dictionary. sys.modules is a cache of all modules that have already been loaded. If the requested module name is in the cache, the interpreter uses the cached module object directly and will not re-execute the module file. This is a key mechanism for avoiding duplicate imports and handling circular dependencies.

  3. Executing __init__.py:

    • When a package (e.g., import my_package) is imported for the first time, the interpreter finds the corresponding directory and executes the __init__.py file within it.
    • The code in __init__.py runs, and any variables, functions, or imported submodule members defined within it become attributes of the package module object.
    • This process only happens on the first import. Subsequent import my_package calls will fetch directly from the sys.modules cache.
  4. Importing a Submodule (import my_package.my_module):

    • The interpreter first ensures the parent package my_package is loaded (executing its __init__.py if not already loaded).
    • It then looks for a my_module.py file inside the my_package directory.
    • Once found, it executes the code in my_module.py and creates a module object for it.
    • This submodule object is then set as an attribute on the my_package module object.

Relative Imports

When modules within the same package refer to each other, using relative imports is a best practice. It avoids hardcoding the top-level package name, making the package easier to rename and move.

  • from . import sibling_module: A single dot . means import from the same directory as the current module (i.e., within the same package).
  • from .sibling_module import some_function: Import a specific member from a sibling module.
  • from .. import parent_package_module: Two dots .. mean import from the parent package (the directory one level up).

Example:

python
# In my_package/string_ops.py
# Import the sibling module math_ops
from . import math_ops 

def process_text(text):
    length = len(text)
    # Use functionality from the sibling module
    return math_ops.add(length, 5)

Note: Relative imports can only be used within a package. They will raise an ImportError if used in a top-level script that is run as the main program.


5. Controlling Python Imports and Exports: Conventions and Special Variables

Unlike JavaScript, which requires the export keyword to explicitly "export" a member, Python's module export control relies more on coding conventions and special variables.

References: Python Tutorial: Modules

1. Single Underscore Prefix (_): A Convention for Internal Use

In Python, if you see a variable, function, or class prefixed with a single underscore (e.g., _internal_helper), it is a strong signal that it is an internal implementation detail of the module and should not be accessed directly by external code.

  • It's a gentleman's agreement: The Python interpreter will not prevent you from importing or accessing these underscored members from the outside. It relies entirely on developer discipline.
  • Effect on wildcard imports: By default, when from my_module import * is used, all names beginning with _ will not be imported.

Example (my_api.py):

python
# my_api.py
def public_api():
    """This is part of the public API."""
    return "This is public."

def _internal_helper():
    """This is an internal helper function."""
    return "This is for internal use."
python
# main.py
from my_api import *

print(public_api()) # -> 'This is public.'
# print(_internal_helper()) # -> NameError: name '_internal_helper' is not defined

# However, you can still explicitly import and use it (not recommended)
from my_api import _internal_helper
print(_internal_helper()) # -> 'This is for internal use.'

2. __all__: The De Facto Standard for Defining a Public API

While the _ convention is useful, it's a method of "exclusion." The __all__ variable provides a "whitelist" mechanism, allowing you to explicitly declare which names should be included in the module's public API.

__all__ is a list of strings defined at the module's top level.

  • Official Role of __all__: Its only official purpose is to control which names are imported when from my_module import * is executed. If __all__ is defined in a module, only the names in the list will be imported.
  • Extended Role of __all__: In the community and toolchains, __all__ is widely considered the de facto standard for defining a module's public API. Static analysis tools (like mypy) and IDEs use it to determine if a name is public, providing more accurate warnings and autocompletion.

Example (my_api_v2.py):

python
# my_api_v2.py
def public_api_one():
    return "Public One"

def public_api_two():
    return "Public Two"

def _internal_api():
    return "Internal"

# We only want public_api_one to be our stable public API
__all__ = ['public_api_one']
python
# main.py
from my_api_v2 import *

print(public_api_one()) # -> 'Public One'
# print(public_api_two()) # -> NameError
# print(_internal_api()) # -> NameError

In this example, even though public_api_two is not prefixed with an underscore, import * does not import it because the presence of __all__ overrides the default behavior.

Comparison Summary with JavaScript

  • Python (Open by Convention):

    • By default, all top-level members in a module not starting with an underscore are accessible from the outside.
    • Privacy is implied by the _ convention.
    • The public API for wildcard imports is suggested and controlled by __all__.
    • Essentially, Python has no truly "private" members that are impossible to access from the outside.
  • JavaScript (Closed by Force):

    • By default, all members in a module are private.
    • You must use the export keyword to enforce which members are public.
    • Members not exported are completely inaccessible to external code, achieving true module-level encapsulation.