The Pope
When pope Francis died in April of 2025, a new pope needed to be elected. The candidate is a member of the papal conclave where the cardinals assemble and elect a person among them. The cardinals are usually known by their birth names. When the elected cardinal becomes pope he also chooses a name for his time serving as pope. The name usually expresses a certain direction that the new pope wants to push the papacy in questions of religion, politics or emphasizing directions in how to positioning the catholic church within the world.
Many popes used a name that was earlier choosen by another pope. This is often done to express believes that the preceedor with the same name shared as well.
When the pope chooces a name that was not taken before, he is referred with the name only, without the ordinal number. The last pope Bergolio had choosen Francis that was not taken from any candidate before. The current pope Leo had choosen a name that was taken before. Therefore the number of Leos in the past is taken, increased by one and appended as the ordinal number. That makes him Leo XIV (the 14th of Leos).
The history knows a view expeption from this. There are among John. There are some numbers being used twice as pope and antipope. The number XX was not officially used at all. So the counting is a bit off. In 1978 John Paul I did actually referred to himself as the first even though the name was not taken before.
Data
According to the history researchers could produce a list of popes. The list was corrected a couple of times because of new investigations and relgious discussions. The list also exists in Wikipedia. To get the list I used the german site: https://de.wikipedia.org/wiki/Liste_der_P%C3%A4pste
The english site also contains a list, however, there is a table for each century that makes it more difficult to handle the data.
In a former arctile about summits in the swiss alps I developed a python script to extract data from a table in a Wikipedia page.
In order to get the data from Wikipedia I used the following command:
/wiki2table.py https://de.wikipedia.org/wiki/Liste_der_P%C3%A4pste --table 1 --cols 1,3,4,6 --format csv
The result is a csv file that looks like this:
Nr. (nach Zählung des Vatikan)[7];Papst;weltlicher Name;Amtszeit;
1;Petrus (hl.);Simon Petrus (aramäisch Keifa, כיפא);0033(?)–67(?);
2;Linus (hl.);;0067(?)–79(?);
3;Anaklet (hl.);;0079(?)–88(?);
...
265;Benedikt XVI.;Joseph Aloisius Ratzinger;2005–2013;
266;Franziskus;Jorge Mario Bergoglio;2013–2025;
267;Leo XIV.;Robert Francis Prevost;2025–;
The script produces different outputs, for simplicity and flexibility I used the CSV format.
Transforming the data
The next part is to read the csv file and basically combine all rows that have the same pope name excluding the ordinal number. This is not that difficult. However, some obstacles needed to be discovered and solved.
The first thing was cleaning the data. Valid data is only given, when the first column contains
a natural number, the numbering of the pope. The list includes some entries that are not
officially recognized as a pope, hence the number is absent. A regular expression matching
numbers only \d+ does the check.
The name column (2) includes some "(hl.)" etc. that are not really helpful and contain
information that I would need in the future process. These need to be excluded.
Actually I did a match on the real name only, assuming a collection of
letters only. The regular expression (\w+( \w+)*) serves this purpose. The brackets collect
the matches and hence the name could be stored from the match in the outer brackets.
The ordinal number can be matched easily. We only need capial letters used for roman numbers. Also we do not need the thoussand (M), five hundred (D), hundred (C) and fifty (L). Only I (one), V (five), and X (ten) need to be matched. The highest number to match is XXIII (of John XXIII). This is far away from 40, that would be written as XL.
The years also need to be handled. I refer to complete years only, so there must be integers for the year starting and ending the pontificate. In the early years the list contains uncertainities such as "0067(?)–79(?)". From that expression the leading zeros and the questions marks need to be cleaned. The result should be 67 and 79 for start and end year. Also, there are pontificates that lasted one year only. In this case start and end year must be the same.
Finally the data of the same pope name without the ordinal number must be combined.
The following python script part handles the parsing part:
csvFile='popes.csv'
popes = {}
with open(csvFile, newline='') as csvfile:
fin = csv.reader(csvfile, delimiter=';')
for row in fin:
# If the pope has no number, then skip this row.
if not re.match("\d+", row[0]):
continue
# Extract and check start and end year of pontificate.
time = row[3].replace('(?)', '')
time = re.sub("^0+", '', time)
time = re.sub("\-0+", '-', time)
times = re.split("[^0-9\-]+", time)
if len(times) == 1:
times.append(times[0])
if times[1] == '':
times[1] = times[0]
[start, end] = map(lambda x: int(x), times)
if end < minYear or start > maxYear:
continue
# From the popes name e.g. Paul IV (hl.) extract main name "Paul" and
# ordinary number IV as own fields.
m = re.match("(\w+( \w+)*)", row[1])
name = m[1]
m = re.search(" ([IVX]+)$", name)
ord = m[1] if m else ''
main = name.replace(" " + ord, '')
# Create a new property with a list for a new main name.
if not main in popes:
popes[main] = []
# Add the current entry to the list.
popes[main].append({
"name": name,
"ord": ord,
"pers": row[2],
"time": {
"start": start,
"end": end
}
})
The result is a dictionary with pope names (e.g. Paul, John, ...) as the key and a list with an entry for each pope:
[
{
'name': 'Felix I',
'ord': 'I',
'pers': '',
'time': {
'start': 269,
'end': 274
}
}, {
'name':
'Felix II',
'ord': 'II',
'pers': '',
'time': {
'start': 483,
'end': 492
}
}, {
'name': 'Felix III',
'ord': 'III',
'pers': '',
'time': {
'start': 526,
'end': 530
}
}
]
The pers property holds the born name, which is empty in the sample. It's empty for
all early popes except the first, which was Simon Pretrus according to the Bible.
There was an issue with John Paul II, the matching didn't work so I checked the data. Apparently in the wikipedia, there is a invisible character, that tampers the matching. I simply deleted the name manually in the CSV file and wrote it again, thus eliminating the weird char. There might be some other errors, I didn't do a sopfisticated check to find all probematic chars, in case there are any.
Building the chart
Having the data in the form bundled by popes and their names, we can now create a chart. The x-axis contains the time, the y-axis contains the "main" names of the popes, e.g. John or Paul. A bar is drawn in the line of the pope and the time of the pontificate. Bars in the same line mean, the popes have choosen the same name for their pontificate.
The y-axis is moved to the right, with an offset so that there is space for the labels that carry the popes names on the y-axis. From the remaining space, the start and end time is used for the length of the x-axis. For visability, a bar has to be at least a length of 1px even if the time of the pontificat would be to short to be visible. The next subsequent pontificat is then started next to that bar in the same line moving the real start by the 1px offset. This is necessary e.g. to make the pontificates of the John Pauls visible.
The chart is written as a svg. Axis and tics are done with the line element, the
bars are drawn with the rect element, each rect element contains a title element
that should show a tooltip when hovering the bar. axis labels are done with a text
element.
The tics are drawn in some logic depending on the desired timeframe. The goal is not to have too many tics and have a reasonable scale. The same counts for the labels. With an unfortunate selection of the parameters, you still might find labels overlapping each other and have a bad rendering result.
The whole script is here:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import csv
import re
import math
# csv file
csvFile = 'popes.csv'
#csvFile = 'johpaul.csv'
# chart
barHeight = 20
barMargin = 3
legendWidth = 120
chartWidth = 1800
maxYear = 2023
minYear = 1
lineColor = '#333333'
colors = ['4287f5', '42e0f5', '425af5']
svg = ''
popes = {}
with open(csvFile, newline='') as csvfile:
fin = csv.reader(csvfile, delimiter=';')
for row in fin:
# If the pope has no number, then skip this row.
if not re.match("\d+", row[0]):
continue
# Extract and check start and end year of pontificate.
time = row[3].replace('(?)', '')
time = re.sub("^0+", '', time)
time = re.sub("\-0+", '-', time)
times = re.split("[^0-9\-]+", time)
if len(times) == 1:
times.append(times[0])
if times[1] == '':
times[1] = times[0]
[start, end] = map(lambda x: int(x), times)
if end < minYear or start > maxYear:
continue
# From the popes name e.g. Paul IV (hl.) extract main name "Paul" and
# ordinary number IV as own fields.
m = re.match("(\w+( \w+)*)", row[1])
name = m[1]
m = re.search(" ([IVX]+)$", name)
ord = m[1] if m else ''
main = name.replace(" " + ord, '')
# Create a new property with a list for a new main name.
if not main in popes:
popes[main] = []
# Add the current entry to the list.
popes[main].append({
"name": name,
"ord": ord,
"pers": row[2],
"time": {
"start": start,
"end": end
}
})
popeNames = popes.keys()
barWidth = chartWidth - legendWidth
yearWidth = maxYear - minYear
chartHeight = (barHeight + barMargin * 2) * len(popeNames) + 20
svg = """<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
version="1.1" baseProfile="full"
width="{0}px" height="{1}px" viewBox="0 0 {0} {1}">
""".format(chartWidth, chartHeight)
for r, main in enumerate(popeNames):
svg += '<text text-anchor="end" x="{0}" y="{1}">{2}</text>'.format(
legendWidth - 5,
(r + 1) * (barHeight + barMargin *2) - 2 * barMargin,
main
)
lastOffset = 0
for i, pope in enumerate(popes[main]):
xs = math.floor(barWidth * (max(pope['time']['start'], minYear) - minYear) / yearWidth)
xe = math.floor(barWidth * (min(pope['time']['end'], maxYear) - minYear) / yearWidth)
width = xe - xs
if width < 1:
width = 1
xe += 1
if xs <= lastOffset:
xs = lastOffset + 1
if xe <= xs:
xe = xs + 1
lastOffset = xe
svg += '<rect x="{0}" y="{1}" width="{2}" height="{3}" fill="#{4}"><title>{5}: {6} - {7}</title></rect>'.format(
math.floor(xs) + legendWidth,
(r * barHeight + r * barMargin * 2) + barMargin,
width,
barHeight,
colors[i % 3],
pope['name'],
pope['time']['start'],
pope['time']['end']
)
#print("{0}\t{1}".format(pope['ord'], pope['time']))
# y-axis next to the legend
svg += '<line x1="{0}" y1="{1}" x2="{0}" y2="{2}" style="stroke:{3};stroke-width:1" />'.format(
legendWidth,
barMargin,
chartHeight - 15,
lineColor
)
# x-axis with time scale.
svg += '<line x1="{0}" y1="{2}" x2="{1}" y2="{2}" style="stroke:{3};stroke-width:1" />'.format(
legendWidth,
chartWidth,
chartHeight - 15,
lineColor
)
timeSpan = maxYear - minYear
factorLabel = 100
factorTick = 50
if timeSpan <= 50:
factorLabel = 5
factorTick = 5
elif timeSpan <= 100:
factorLabel = 10
factorTick = 5
elif timeSpan <= 200:
factorLabel = 20
factorTick = 10
elif timeSpan <= 500:
factorLabel = 50
factorTick = 20
tickYear = math.ceil(minYear / factorTick) * factorTick
while tickYear < maxYear:
svg += '<line x1="{0}" y1="{1}" x2="{0}" y2="{2}" style="stroke:{3};stroke-width:1"/>'.format(
math.floor(barWidth * (tickYear - minYear) / yearWidth) + legendWidth,
chartHeight - 20,
chartHeight - 10,
lineColor
)
if tickYear % factorLabel == 0:
svg += '<text text-anchor="middle" x="{0}" y="{1}">{2}</text>'.format(
math.floor(barWidth * (tickYear - minYear) / yearWidth) + legendWidth,
chartHeight,
tickYear
)
tickYear += factorTick
svg += ''
svg += '</svg>'
print(svg)
The SVG data is printed to stdout, so it makes sense to redirect the output into a file. There are also some warnings printed on stderr that I wasn't able to avoid.
Interactive webchart
When creating different charts, the parameters need to be adjusted in the top section of the script. I didn't wanted to make it much longer and having the command line processing in the script as well. Therefore, I created a webversion with Javascript that does pretty much the same. The first time I used ChatGPT to transfer parts of the code from Python into Javascript. This worked pretty well and I didn't have to make any adjustments to the resulting code.
The Pontificate chart tool let's you more easily define the parameters in a web frontend. The resulting svg is the same as with the Python tool.