diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 58535f98..690c1327 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -70,7 +70,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install flake8 pytest pytest-cov
+          pip install flake8 pytest pytest-cov nbval numpy
           cp pysrc/juliacall/juliapkg-dev.json pysrc/juliacall/juliapkg.json
           pip install -e .
       - name: Lint with flake8
@@ -81,7 +81,7 @@ jobs:
           flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
       - name: Run tests
         run: |
-          pytest -s --cov=pysrc
+          pytest -s --nbval --cov=pysrc ./pytest/
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v2
         env:
diff --git a/docs/src/compat.md b/docs/src/compat.md
index 7df0e235..751a2053 100644
--- a/docs/src/compat.md
+++ b/docs/src/compat.md
@@ -67,8 +67,9 @@ The `juliacall` IPython extension adds these features to your IPython session:
 
 The extension is experimental and unstable - the API can change at any time.
 
-Enable the extension with `%load_ext juliacall`.
-See [the IPython docs](https://ipython.readthedocs.io/en/stable/config/extensions/).
+You can explicitly enable the extension with `%load_ext juliacall`, but
+it will automatically be loaded if `juliacall` is imported and IPython is detected.
+You can disable this behavior with an [environment variable](@ref julia-config).
 
 The `%%julia` cell magic can synchronise variables between Julia and Python by listing them
 on the first line:
@@ -88,6 +89,9 @@ In [5]: z
 Out[5]: '2^8 = 256'
 ```
 
+Also see [the IPython docs](https://ipython.readthedocs.io/en/stable/config/extensions/)
+for more information on extensions.
+
 ## Asynchronous Julia code (including Makie)
 
 Asynchronous Julia code will not normally run while Python is executing, unless it is in a
diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md
index 98d74784..a16b71f2 100644
--- a/docs/src/juliacall.md
+++ b/docs/src/juliacall.md
@@ -123,3 +123,4 @@ be configured in two ways:
 | `-X juliacall-sysimage=<file>` | `PYTHON_JULIACALL_SYSIMAGE=<file>` | Use the given system image. |
 | `-X juliacall-threads=<N\|auto>` | `PYTHON_JULIACALL_THREADS=<N\|auto>` | Launch N threads. |
 | `-X juliacall-warn-overwrite=<yes\|no>` | `PYTHON_JULIACALL_WARN_OVERWRITE=<yes\|no>` | Enable or disable method overwrite warnings. |
+| `-X juliacall-autoload-ipython-extension=<yes\|no>` | `PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=<yes\|no>` | Enable or disable IPython extension autoloading. |
diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py
index d9e7a594..54b1100f 100644
--- a/pysrc/juliacall/__init__.py
+++ b/pysrc/juliacall/__init__.py
@@ -248,8 +248,30 @@ def jlstr(x):
                 "PYTHON_JULIACALL_HANDLE_SIGNALS=no."
             )
 
-init()
+    # Next, automatically load the juliacall extension if we are in IPython or Jupyter
+    CONFIG['autoload_ipython_extension'] = choice('autoload_ipython_extension', ['yes', 'no'])[0]
+    if CONFIG['autoload_ipython_extension'] in {'yes', None}:
+        try:
+            get_ipython = sys.modules['IPython'].get_ipython
+
+            if CONFIG['autoload_ipython_extension'] is None:
+                # Only let the user know if it was not explicitly set
+                print(
+                    "Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython"
+                )
+
+            load_ipython_extension(get_ipython())
+        except Exception as e:
+            if CONFIG['autoload_ipython_extension'] == 'yes':
+                # Only warn if the user explicitly requested the extension to be loaded
+                warnings.warn(
+                    "Could not load juliacall extension in Jupyter notebook: " + str(e)
+                )
+            pass
+
 
 def load_ipython_extension(ip):
     import juliacall.ipython
     juliacall.ipython.load_ipython_extension(ip)
+
+init()
diff --git a/pytest/test_nb.ipynb b/pytest/test_nb.ipynb
new file mode 100644
index 00000000..f7e2b98f
--- /dev/null
+++ b/pytest/test_nb.ipynb
@@ -0,0 +1,139 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NBVAL_IGNORE_OUTPUT\n",
+    "import numpy as np\n",
+    "from juliacall import Main as jl"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "3\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%julia\n",
+    "\n",
+    "# Automatically activates Julia magic\n",
+    "\n",
+    "x = 1\n",
+    "println(x + 2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "4\n"
+     ]
+    }
+   ],
+   "source": [
+    "%julia println(x + 3)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "1.0"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "jl.cos(0)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "f (generic function with 1 method)"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "%%julia\n",
+    "\n",
+    "function f(x::AbstractArray)\n",
+    "    sum(x)\n",
+    "end"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "6"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "jl.f(np.array([1, 2, 3]))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.10.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}