{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Packages and namespaces\n",
    "\n",
    "A module is a single `.py` file. A *package* is a directory of\n",
    "modules, treated as a single importable unit. Most non-trivial\n",
    "Python projects are packages — including almost everything on PyPI.\n",
    "This tutorial covers how packages work, the role of `__init__.py`,\n",
    "and the difference between absolute and relative imports.\n",
    "\n",
    "**Time commitment:** 15-20 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- You've worked through [Modules and imports](https://agilearn.co.uk/guides/packages-and-packaging/learn/01-modules-and-imports)\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Lay out a package directory and explain the role of `__init__.py`\n",
    "- Import from sub-packages with both absolute and relative imports\n",
    "- Recognise and use namespace packages"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## From module to package\n",
    "\n",
    "A package is a directory containing an `__init__.py` file. The\n",
    "directory name is the package name, and the package's modules are\n",
    "the `.py` files inside it. Sub-directories with their own\n",
    "`__init__.py` files become sub-packages.\n",
    "\n",
    "Here's the layout we'll build in this notebook:\n",
    "\n",
    "```text\n",
    "shapes/\n",
    "    __init__.py\n",
    "    square.py\n",
    "    polygons/\n",
    "        __init__.py\n",
    "        triangle.py\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building it on the page\n",
    "\n",
    "We can do this in the browser because Pyodide gives us a virtual\n",
    "filesystem — `os.makedirs` and `open(...).write()` work just as they\n",
    "would on disk. The files we create live in the page's memory and\n",
    "disappear when you reload."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, sys\n",
    "\n",
    "# Make sure the current directory is on the import path.\n",
    "if \"\" not in sys.path:\n",
    "    sys.path.insert(0, \"\")\n",
    "\n",
    "os.makedirs(\"shapes/polygons\", exist_ok=True)\n",
    "\n",
    "# __init__.py marks the directory as a package. It can be empty.\n",
    "open(\"shapes/__init__.py\", \"w\").close()\n",
    "open(\"shapes/polygons/__init__.py\", \"w\").close()\n",
    "\n",
    "# A simple module inside the package.\n",
    "open(\"shapes/square.py\", \"w\").write(\n",
    "    \"def area(side):\\n\"\n",
    "    \"    return side * side\\n\"\n",
    ")\n",
    "\n",
    "# And one inside the sub-package.\n",
    "open(\"shapes/polygons/triangle.py\", \"w\").write(\n",
    "    \"def area(base, height):\\n\"\n",
    "    \"    return 0.5 * base * height\\n\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# We can now import from our package as if it were any third-party library.\n",
    "import shapes.square\n",
    "import shapes.polygons.triangle\n",
    "\n",
    "print(shapes.square.area(5))\n",
    "print(shapes.polygons.triangle.area(4, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What `__init__.py` is for\n",
    "\n",
    "At minimum, `__init__.py` marks the directory as a package. But it's\n",
    "also code that runs when the package is imported, which makes it the\n",
    "natural home for a few things:\n",
    "\n",
    "- **Re-exports.** Make a sub-module's name available at the package\n",
    "  level: `from .square import area`. Now `shapes.area` works without\n",
    "  the user knowing `square.py` exists.\n",
    "- **Public API definition.** Set `__all__ = [\"area\", ...]` to declare\n",
    "  which names should be considered the package's public surface.\n",
    "- **Package-level constants.** A `__version__` string is conventional.\n",
    "\n",
    "Avoid putting anything heavy or slow in `__init__.py` — it runs every\n",
    "time anyone imports anything from the package."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Promote `area` to the package level by editing __init__.py.\n",
    "open(\"shapes/__init__.py\", \"w\").write(\n",
    "    \"from .square import area as square_area\\n\"\n",
    "    \"from .polygons.triangle import area as triangle_area\\n\"\n",
    "    \"__version__ = \\\"0.1.0\\\"\\n\"\n",
    ")\n",
    "\n",
    "# Force a fresh import so __init__.py runs again.\n",
    "import importlib, shapes\n",
    "importlib.reload(shapes)\n",
    "\n",
    "print(shapes.square_area(5))\n",
    "print(shapes.triangle_area(4, 3))\n",
    "print(shapes.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Absolute vs relative imports\n",
    "\n",
    "Inside the package, modules can refer to each other in two ways.\n",
    "\n",
    "**Absolute** imports name the package from the top:\n",
    "\n",
    "```python\n",
    "# inside shapes/polygons/triangle.py\n",
    "from shapes.square import area\n",
    "```\n",
    "\n",
    "**Relative** imports use dots to mean \"current package\" and \"parent\n",
    "package\":\n",
    "\n",
    "```python\n",
    "# inside shapes/polygons/triangle.py\n",
    "from ..square import area    # one dot up to `shapes`, then into `square`\n",
    "from . import other_module   # same package as me\n",
    "```\n",
    "\n",
    "Style guides (including PEP 8) recommend absolute imports for\n",
    "clarity. Relative imports are useful when a package might be renamed\n",
    "or vendored, since they don't hard-code the top-level name. Pick one\n",
    "style per project and be consistent."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Namespace packages\n",
    "\n",
    "Since Python 3.3, a directory can be importable as a package\n",
    "*without* an `__init__.py` — a so-called **namespace package**. This\n",
    "is mostly useful when you want a single logical package whose contents\n",
    "live in multiple physical directories (often on different `sys.path`\n",
    "entries), typically for plugin-style architectures.\n",
    "\n",
    "For everyday work, prefer regular packages with an `__init__.py` —\n",
    "they're explicit, support per-package initialisation, and avoid a few\n",
    "subtle gotchas around `sys.path` ordering."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap and next steps\n",
    "\n",
    "- A package is a directory with an `__init__.py` (regular) or without\n",
    "  (namespace).\n",
    "- `__init__.py` is the natural place for re-exports, `__all__`, and\n",
    "  version constants.\n",
    "- Prefer absolute imports for clarity; reach for relative imports when\n",
    "  the package's name isn't fixed.\n",
    "\n",
    "Next: how to bring code from outside your project into it —\n",
    "[Installing third-party packages](https://agilearn.co.uk/guides/packages-and-packaging/learn/03-installing-third-party-packages)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}