This page was generated from doc/code-cells.ipynb. Interactive online version: Binder badge. Download notebook.

Code Cells

Code, Output, Streams

An empty code cell:

[ ]:

Two empty lines:

[ ]:

Leading/trailing empty lines:

[1]:


# 2 empty lines before, 1 after

A simple output:

[2]:
6 * 7
[2]:
42

The standard output stream:

[3]:
print('Hello, world!')
Hello, world!

Normal output + standard output

[4]:
print('Hello, world!')
6 * 7
Hello, world!
[4]:
42

The standard error stream is highlighted and displayed just below the code cell. The standard output stream comes afterwards (with no special highlighting). Finally, the “normal” output is displayed.

[5]:
import sys

print("I'll appear on the standard error stream", file=sys.stderr)
print("I'll appear on the standard output stream")
"I'm the 'normal' output"
I'll appear on the standard output stream
I'll appear on the standard error stream
[5]:
"I'm the 'normal' output"

Note

Using the IPython kernel, the order is actually mixed up, see https://github.com/ipython/ipykernel/issues/280.

Special Display Formats

See IPython example notebook.

Local Image Files

[6]:
from IPython.display import Image
i = Image(filename='images/notebook_icon.png')
i
[6]:
_images/code-cells_18_0.png
[7]:
display(i)
_images/code-cells_19_0.png

See also SVG support for LaTeX.

[8]:
from IPython.display import SVG
SVG(filename='images/python_logo.svg')
[8]:
_images/code-cells_21_0.svg

Image URLs

There may be errors in this part because images’ URLs are not accessible during a build of RPM. See online documentation.

[9]:
Image(url='https://www.python.org/static/img/python-logo-large.png')
[9]:
[10]:
Image(url='https://www.python.org/static/img/python-logo-large.png', embed=True)
---------------------------------------------------------------------------
gaierror                                  Traceback (most recent call last)
File /usr/lib64/python3.13/urllib/request.py:1319, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1318 try:
-> 1319     h.request(req.get_method(), req.selector, req.data, headers,
   1320               encode_chunked=req.has_header('Transfer-encoding'))
   1321 except OSError as err: # timeout error

File /usr/lib64/python3.13/http/client.py:1336, in HTTPConnection.request(self, method, url, body, headers, encode_chunked)
   1335 """Send a complete request to the server."""
-> 1336 self._send_request(method, url, body, headers, encode_chunked)

File /usr/lib64/python3.13/http/client.py:1382, in HTTPConnection._send_request(self, method, url, body, headers, encode_chunked)
   1381     body = _encode(body, 'body')
-> 1382 self.endheaders(body, encode_chunked=encode_chunked)

File /usr/lib64/python3.13/http/client.py:1331, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1330     raise CannotSendHeader()
-> 1331 self._send_output(message_body, encode_chunked=encode_chunked)

File /usr/lib64/python3.13/http/client.py:1091, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1090 del self._buffer[:]
-> 1091 self.send(msg)
   1093 if message_body is not None:
   1094
   1095     # create a consistent interface to message_body

File /usr/lib64/python3.13/http/client.py:1035, in HTTPConnection.send(self, data)
   1034 if self.auto_open:
-> 1035     self.connect()
   1036 else:

File /usr/lib64/python3.13/http/client.py:1470, in HTTPSConnection.connect(self)
   1468 "Connect to a host on a given (SSL) port."
-> 1470 super().connect()
   1472 if self._tunnel_host:

File /usr/lib64/python3.13/http/client.py:1001, in HTTPConnection.connect(self)
   1000 sys.audit("http.client.connect", self, self.host, self.port)
-> 1001 self.sock = self._create_connection(
   1002     (self.host,self.port), self.timeout, self.source_address)
   1003 # Might fail in OSs that don't implement TCP_NODELAY

File /usr/lib64/python3.13/socket.py:828, in create_connection(address, timeout, source_address, all_errors)
    827 exceptions = []
--> 828 for res in getaddrinfo(host, port, 0, SOCK_STREAM):
    829     af, socktype, proto, canonname, sa = res

File /usr/lib64/python3.13/socket.py:963, in getaddrinfo(host, port, family, type, proto, flags)
    962 addrlist = []
--> 963 for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
    964     af, socktype, proto, canonname, sa = res

gaierror: [Errno -3] Temporary failure in name resolution

During handling of the above exception, another exception occurred:

URLError                                  Traceback (most recent call last)
Cell In[10], line 1
----> 1 Image(url='https://www.python.org/static/img/python-logo-large.png', embed=True)

File /usr/lib/python3.13/site-packages/IPython/core/display.py:970, in Image.__init__(self, data, url, filename, format, embed, width, height, retina, unconfined, metadata, alt)
    968 self.unconfined = unconfined
    969 self.alt = alt
--> 970 super(Image, self).__init__(data=data, url=url, filename=filename,
    971         metadata=metadata)
    973 if self.width is None and self.metadata.get('width', {}):
    974     self.width = metadata['width']

File /usr/lib/python3.13/site-packages/IPython/core/display.py:327, in DisplayObject.__init__(self, data, url, filename, metadata)
    324 elif self.metadata is None:
    325     self.metadata = {}
--> 327 self.reload()
    328 self._check_data()

File /usr/lib/python3.13/site-packages/IPython/core/display.py:1005, in Image.reload(self)
   1003 """Reload the raw data from file or URL."""
   1004 if self.embed:
-> 1005     super(Image,self).reload()
   1006     if self.retina:
   1007         self._retina_shape()

File /usr/lib/python3.13/site-packages/IPython/core/display.py:358, in DisplayObject.reload(self)
    355 elif self.url is not None:
    356     # Deferred import
    357     from urllib.request import urlopen
--> 358     response = urlopen(self.url)
    359     data = response.read()
    360     # extract encoding from header, if there is one:

File /usr/lib64/python3.13/urllib/request.py:189, in urlopen(url, data, timeout, context)
    187 else:
    188     opener = _opener
--> 189 return opener.open(url, data, timeout)

File /usr/lib64/python3.13/urllib/request.py:489, in OpenerDirector.open(self, fullurl, data, timeout)
    486     req = meth(req)
    488 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 489 response = self._open(req, data)
    491 # post-process response
    492 meth_name = protocol+"_response"

File /usr/lib64/python3.13/urllib/request.py:506, in OpenerDirector._open(self, req, data)
    503     return result
    505 protocol = req.type
--> 506 result = self._call_chain(self.handle_open, protocol, protocol +
    507                           '_open', req)
    508 if result:
    509     return result

File /usr/lib64/python3.13/urllib/request.py:466, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    464 for handler in handlers:
    465     func = getattr(handler, meth_name)
--> 466     result = func(*args)
    467     if result is not None:
    468         return result

File /usr/lib64/python3.13/urllib/request.py:1367, in HTTPSHandler.https_open(self, req)
   1366 def https_open(self, req):
-> 1367     return self.do_open(http.client.HTTPSConnection, req,
   1368                         context=self._context)

File /usr/lib64/python3.13/urllib/request.py:1322, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1319         h.request(req.get_method(), req.selector, req.data, headers,
   1320                   encode_chunked=req.has_header('Transfer-encoding'))
   1321     except OSError as err: # timeout error
-> 1322         raise URLError(err)
   1323     r = h.getresponse()
   1324 except:

URLError: <urlopen error [Errno -3] Temporary failure in name resolution>
[11]:
Image(url='https://jupyter.org/assets/homepage/main-logo.svg')
[11]:

Math

[12]:
from IPython.display import Math
eq = Math(r'\int\limits_{-\infty}^\infty f(x) \delta(x - x_0) dx = f(x_0)')
eq
[12]:
$\displaystyle \int\limits_{-\infty}^\infty f(x) \delta(x - x_0) dx = f(x_0)$
[13]:
display(eq)
$\displaystyle \int\limits_{-\infty}^\infty f(x) \delta(x - x_0) dx = f(x_0)$
[14]:
from IPython.display import Latex
Latex(r'This is a \LaTeX{} equation: $a^2 + b^2 = c^2$')
[14]:
This is a \LaTeX{} equation: $a^2 + b^2 = c^2$
[15]:
%%latex
\begin{equation}
\int\limits_{-\infty}^\infty f(x) \delta(x - x_0) dx = f(x_0)
\end{equation}
\begin{equation} \int\limits_{-\infty}^\infty f(x) \delta(x - x_0) dx = f(x_0) \end{equation}

Plots

Make sure to use at least version 0.1.6 of the matplotlib-inline package (which is an automatic dependency of the ipython package).

By default, the plots created with the “inline” backend have the wrong size. More specifically, PNG plots (the default) will be slightly larger than SVG and PDF plots.

This can be fixed easily by creating a file named matplotlibrc (in the directory where your Jupyter notebooks live, e.g. in this directory: matplotlibrc) and adding the following line:

figure.dpi: 96

If you are using Git to manage your files, don’t forget to commit this local configuration file to your repository. Different directories can have different local configurations. If a given configuration should apply to multiple directories, symbolic links can be created in each directory.

For more details, see Default Values for Matplotlib’s “inline” Backend.

By default, plots are generated in the PNG format. In most cases, it looks better if SVG plots are used for HTML output and PDF plots are used for LaTeX/PDF. This can be achieved by setting nbsphinx_execute_arguments in your conf.py file like this:

nbsphinx_execute_arguments = [
    "--InlineBackend.figure_formats={'svg', 'pdf'}",
]

In the following example, nbsphinx should use an SVG image in the HTML output and a PDF image for LaTeX/PDF output (other Jupyter clients like JupyterLab will still show the default PNG format).

[16]:
import matplotlib.pyplot as plt
[17]:
fig, ax = plt.subplots(figsize=[6, 3])
ax.plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8]);
_images/code-cells_37_0.svg

For comparison, this is how it would look in PNG format …

[18]:
%config InlineBackend.figure_formats = ['png']
[19]:
fig
[19]:
_images/code-cells_40_0.png

… and in 'png2x' (a.k.a. 'retina') format:

[20]:
%config InlineBackend.figure_formats = ['png2x']
[21]:
fig
[21]:
_images/code-cells_43_0.png

Instead of the default inline plotting backend, you can also use the widget backend (which needs the ipympl package to be installed):

[22]:
%matplotlib widget
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
File /usr/lib64/python3.13/site-packages/matplotlib/backends/registry.py:413, in BackendRegistry.resolve_gui_or_backend(self, gui_or_backend)
    412 try:
--> 413     return self.resolve_backend(gui_or_backend)
    414 except Exception:  # KeyError ?

File /usr/lib64/python3.13/site-packages/matplotlib/backends/registry.py:375, in BackendRegistry.resolve_backend(self, backend)
    374 if gui is None:
--> 375     raise RuntimeError(f"'{backend}' is not a recognised backend name")
    377 return backend, gui if gui != "headless" else None

RuntimeError: 'widget' is not a recognised backend name

During handling of the above exception, another exception occurred:

RuntimeError                              Traceback (most recent call last)
Cell In[22], line 1
----> 1 get_ipython().run_line_magic('matplotlib', 'widget')

File /usr/lib/python3.13/site-packages/IPython/core/interactiveshell.py:2480, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
   2478     kwargs['local_ns'] = self.get_local_scope(stack_depth)
   2479 with self.builtin_trap:
-> 2480     result = fn(*args, **kwargs)
   2482 # The code below prevents the output from being displayed
   2483 # when using magics with decorator @output_can_be_silenced
   2484 # when the last Python token in the expression is a ';'.
   2485 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /usr/lib/python3.13/site-packages/IPython/core/magics/pylab.py:103, in PylabMagics.matplotlib(self, line)
     98     print(
     99         "Available matplotlib backends: %s"
    100         % _list_matplotlib_backends_and_gui_loops()
    101     )
    102 else:
--> 103     gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui)
    104     self._show_matplotlib_backend(args.gui, backend)

File /usr/lib/python3.13/site-packages/IPython/core/interactiveshell.py:3665, in InteractiveShell.enable_matplotlib(self, gui)
   3662     import matplotlib_inline.backend_inline
   3664 from IPython.core import pylabtools as pt
-> 3665 gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)
   3667 if gui != None:
   3668     # If we have our first gui selection, store it
   3669     if self.pylab_gui_select is None:

File /usr/lib/python3.13/site-packages/IPython/core/pylabtools.py:349, in find_gui_and_backend(gui, gui_select)
    347 else:
    348     gui = _convert_gui_to_matplotlib(gui)
--> 349     backend, gui = backend_registry.resolve_gui_or_backend(gui)
    351 gui = _convert_gui_from_matplotlib(gui)
    352 return gui, backend

File /usr/lib64/python3.13/site-packages/matplotlib/backends/registry.py:415, in BackendRegistry.resolve_gui_or_backend(self, gui_or_backend)
    413     return self.resolve_backend(gui_or_backend)
    414 except Exception:  # KeyError ?
--> 415     raise RuntimeError(
    416         f"'{gui_or_backend} is not a recognised GUI loop or backend name")

RuntimeError: 'widget is not a recognised GUI loop or backend name
[23]:
fig, ax = plt.subplots(figsize=[6, 3])
ax.plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8]);
_images/code-cells_46_0.png

Pandas Dataframes

Pandas dataframes should be displayed as nicely formatted HTML tables (if you are using HTML output).

[24]:
import numpy as np
import pandas as pd
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[24], line 2
      1 import numpy as np
----> 2 import pandas as pd

ModuleNotFoundError: No module named 'pandas'
[25]:
df = pd.DataFrame(np.random.randint(0, 100, size=[10, 4]),
                  columns=[r'$\alpha$', r'$\beta$', r'$\gamma$', r'$\delta$'])
df
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 df = pd.DataFrame(np.random.randint(0, 100, size=[10, 4]),
      2                   columns=[r'$\alpha$', r'$\beta$', r'$\gamma$', r'$\delta$'])
      3 df

NameError: name 'pd' is not defined

Markdown Content

[26]:
from IPython.display import Markdown
[27]:
md = Markdown("""
# Markdown

It *should* show up as **formatted** text
with things like [links] and images.

[links]: https://jupyter.org/

![Jupyter notebook icon](images/notebook_icon.png)

## Markdown Extensions

There might also be mathematical equations like
$a^2 + b^2 = c^2$
and even tables:

A     | B     | A and B
------|-------|--------
False | False | False
True  | False | False
False | True  | False
True  | True  | True

""")
md
[27]:

Markdown

It should show up as formatted text with things like links and images.

Jupyter notebook icon
Markdown Extensions

There might also be mathematical equations like \(a^2 + b^2 = c^2\) and even tables:

A

B

A and B

False

False

False

True

False

False

False

True

False

True

True

True

YouTube Videos

[28]:
from IPython.display import YouTubeVideo
YouTubeVideo('9_OIs49m56E')
[28]:

Interactive Widgets (HTML only)

The basic widget infrastructure is provided by the ipywidgets module. More advanced widgets are available in separate packages, see for example https://jupyter.org/widgets.

The JavaScript code which is needed to display Jupyter widgets is loaded automatically (using RequireJS). If you want to use non-default URLs or local files, you can use the nbsphinx_widgets_path and nbsphinx_requirejs_path settings.

[29]:
import ipywidgets as w
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[29], line 1
----> 1 import ipywidgets as w

ModuleNotFoundError: No module named 'ipywidgets'
[30]:
slider = w.IntSlider()
slider.value = 42
slider
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[30], line 1
----> 1 slider = w.IntSlider()
      2 slider.value = 42
      3 slider

NameError: name 'w' is not defined

A widget typically consists of a so-called “model” and a “view” into that model.

If you display a widget multiple times, all instances act as a “view” into the same “model”. That means that their state is synchronized. You can move either one of these sliders to try this out:

[31]:
slider
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[31], line 1
----> 1 slider

NameError: name 'slider' is not defined

You can also link different widgets.

Widgets can be linked via the kernel (which of course only works while a kernel is running) or directly in the client (which even works in the rendered HTML pages).

Widgets can be linked uni- or bi-directionally.

Examples for all 4 combinations are shown here:

[32]:
link = w.IntSlider(description='link')
w.link((slider, 'value'), (link, 'value'))
jslink = w.IntSlider(description='jslink')
w.jslink((slider, 'value'), (jslink, 'value'))
dlink = w.IntSlider(description='dlink')
w.dlink((slider, 'value'), (dlink, 'value'))
jsdlink = w.IntSlider(description='jsdlink')
w.jsdlink((slider, 'value'), (jsdlink, 'value'))
w.VBox([link, jslink, dlink, jsdlink])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[32], line 1
----> 1 link = w.IntSlider(description='link')
      2 w.link((slider, 'value'), (link, 'value'))
      3 jslink = w.IntSlider(description='jslink')

NameError: name 'w' is not defined
[33]:
tabs = w.Tab()
for idx, obj in enumerate([df, fig, eq, i, md, slider]):
    out = w.Output()
    with out:
        display(obj)
    tabs.children += out,
    tabs.set_title(idx, obj.__class__.__name__)
tabs
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[33], line 1
----> 1 tabs = w.Tab()
      2 for idx, obj in enumerate([df, fig, eq, i, md, slider]):
      3     out = w.Output()

NameError: name 'w' is not defined

Other Languages

The examples shown here are using Python, but the widget technology can also be used with different Jupyter kernels (i.e. with different programming languages).

Troubleshooting

To obtain more information if widgets are not displayed as expected, you will need to look at the error message in the web browser console.

To figure out how to open the web browser console, you may look at the web browser documentation:

The error is most probably linked to the JavaScript files not being loaded or loaded in the wrong order within the HTML file. To analyze the error, you can inspect the HTML file within the web browser (e.g.: right-click on the page and select View Page Source) and look at the <head> section of the page. That section should contain some JavaScript libraries. Those relevant for widgets are:

<!-- require.js is a mandatory dependency for jupyter-widgets -->
<script crossorigin="anonymous" integrity="sha256-Ae2Vz/4ePdIu6ZyI/5ZGsYnb+m0JlOmKPjt6XZ9JJkA=" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js"></script>
<!-- jupyter-widgets JavaScript -->
<script type="text/javascript" src="https://unpkg.com/@jupyter-widgets/html-manager@^0.18.0/dist/embed-amd.js"></script>
<!-- JavaScript containing custom Jupyter widgets -->
<script src="../_static/embed-widgets.js"></script>

The two first elements are mandatory. The third one is required only if you designed your own widgets but did not publish them on npm.js.

If those libraries appear in a different order, the widgets won’t be displayed.

Here is a list of possible solutions:

  • If the widgets are not displayed, see #519.

  • If the widgets are displayed multiple times, see #378.

Arbitrary JavaScript Output (HTML only)

[34]:
%%javascript

var text = document.createTextNode("Hello, I was generated with JavaScript!");
// Content appended to "element" will be visible in the output area:
element.appendChild(text);

Unsupported Output Types

If a code cell produces data with an unsupported MIME type, the Jupyter Notebook doesn’t generate any output. nbsphinx, however, shows a warning message.

[35]:
display({
    'text/x-python': 'print("Hello, world!")',
    'text/x-haskell': 'main = putStrLn "Hello, world!"',
}, raw=True)

Data type cannot be displayed: text/x-python, text/x-haskell

ANSI Colors

The standard output and standard error streams may contain ANSI escape sequences to change the text and background colors.

[36]:
print('BEWARE: \x1b[1;33;41mugly colors\x1b[m!', file=sys.stderr)
print('AB\x1b[43mCD\x1b[35mEF\x1b[1mGH\x1b[4mIJ\x1b[7m'
      'KL\x1b[49mMN\x1b[39mOP\x1b[22mQR\x1b[24mST\x1b[27mUV')
ABCDEFGHIJKLMNOPQRSTUV
BEWARE: ugly colors!

The following code showing the 8 basic ANSI colors is based on https://web.archive.org/web/20231225185739/https://tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html. Each of the 8 colors has an “intense” variation, which is used for bold text.

[37]:
text = ' XYZ '
formatstring = '\x1b[{}m' + text + '\x1b[m'

print(' ' * 6 + ' ' * len(text) +
      ''.join('{:^{}}'.format(bg, len(text)) for bg in range(40, 48)))
for fg in range(30, 38):
    for bold in False, True:
        fg_code = ('1;' if bold else '') + str(fg)
        print(' {:>4} '.format(fg_code) + formatstring.format(fg_code) +
              ''.join(formatstring.format(fg_code + ';' + str(bg))
                      for bg in range(40, 48)))
            40   41   42   43   44   45   46   47
   30  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;30  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   31  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;31  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   32  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;32  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   33  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;33  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   34  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;34  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   35  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;35  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   36  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;36  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
   37  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 
 1;37  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ  XYZ 

ANSI also supports a set of 256 indexed colors. The following code showing all of them is based on http://bitmote.com/index.php?post/2012/11/19/Using-ANSI-Color-Codes-to-Colorize-Your-Bash-Prompt-on-Linux.

[38]:
formatstring = '\x1b[38;5;{0};48;5;{0}mX\x1b[1mX\x1b[m'

print('  + ' + ''.join('{:2}'.format(i) for i in range(36)))
print('  0 ' + ''.join(formatstring.format(i) for i in range(16)))
for i in range(7):
    i = i * 36 + 16
    print('{:3} '.format(i) + ''.join(formatstring.format(i + j)
                                      for j in range(36) if i + j < 256))
  +  0 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435
  0 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 16 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 52 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 88 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
124 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
160 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
196 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
232 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You can even use 24-bit RGB colors:

[39]:
start = 255, 0, 0
end = 0, 0, 255
length = 79
out = []

for i in range(length):
    rgb = [start[c] + int(i * (end[c] - start[c]) / length) for c in range(3)]
    out.append('\x1b['
               '38;2;{rgb[2]};{rgb[1]};{rgb[0]};'
               '48;2;{rgb[0]};{rgb[1]};{rgb[2]}mX\x1b[m'.format(rgb=rgb))
print(''.join(out))
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX