Usage

Table of contents

  1. Backend Selection
  2. Get Tables and Variables
  3. Get Data of a Table
  4. Use Together with pandas
  5. Large Queries
  6. OData Backend: Substrings
  7. OData Backend: Date Ranges
  8. OData Backend: Advanced Filter
  9. Visualize Voting Results
  10. OpenParlData Backend
    1. Search with the OpenParlData Backend
    2. limit is a Page Size, Not a Cap
    3. Case Sensitivity of search_mode="exact"
    4. Get Related Data
  11. API Reference

Backend Selection

swissparlpy supports two data backends:

Backend Description
odata (default) Official OData API of parlament.ch
openparldata REST API of OpenParlData.ch

Using the default OData backend:

import swissparlpy as spp

tables = spp.get_tables()  # uses OData backend by default
print(tables)
Output
['MemberParty', 'Party', 'Person', 'PersonAddress', 'PersonCommunication', 'PersonInterest', 'Session', 'Committee', 'MemberCommittee', 'Canton', 'Council', 'Objective', 'Resolution', 'Publication', 'External', 'Meeting', 'Subject', 'Citizenship', 'Preconsultation', 'Bill', 'BillLink', 'BillStatus', 'Business', 'BusinessResponsibility', 'BusinessRole', 'LegislativePeriod', 'MemberCouncil', 'MemberParlGroup', 'ParlGroup', 'PersonOccupation', 'RelatedBusiness', 'BusinessStatus', 'BusinessType', 'MemberCouncilHistory', 'MemberCommitteeHistory', 'Vote', 'Voting', 'SubjectBusiness', 'Transcript', 'ParlGroupHistory', 'Tags', 'SeatOrganisationNr', 'PersonEmployee', 'Rapporteur', 'Mutation', 'SeatOrganisationSr', 'MemberParlGroupHistory', 'MemberPartyHistory']

Using the OpenParlData backend:

import swissparlpy as spp

tables = spp.get_tables(backend='openparldata')
print(tables)
Output
['bodies', 'speeches', 'persons', 'groups', 'meetings', 'agendas', 'texts', 'votes', 'docs', 'affairs', 'votings', 'interests', 'events', 'external_links', 'contributors', 'person_images', 'memberships', 'access_badges']

Using the SwissParlClient class:

from swissparlpy import SwissParlClient

# OData backend
odata_client = SwissParlClient(backend="odata")
print(odata_client.get_tables())

# OpenParlData backend
opd_client = SwissParlClient(backend="openparldata")
print(opd_client.get_tables())

All module-level functions (get_tables(), get_variables(), get_overview(), get_glimpse(), get_data()) accept a backend parameter.


Get Tables and Variables

import swissparlpy as spp

# List the first 5 available tables
spp.get_tables()[:5]
# ['MemberParty', 'Party', 'Person', 'PersonAddress', 'PersonCommunication']

# Get the variables (columns) of a table
spp.get_variables('Party')
# ['ID', 'Language', 'PartyNumber', 'PartyName', 'StartDate', 'EndDate', 'Modified', 'PartyAbbreviation']

Get Data of a Table

import swissparlpy as spp

# Fetch councillors with a filter
councillors = spp.get_data('MemberCouncil', Language='DE', CantonAbbreviation='ZH')
print(councillors.count)
for c in councillors:
    print(c['LastName'], c['FirstName'])

Every keyword argument of get_data() is used as a filter on the corresponding variable of the table.

The result is iterable, indexable (data[0], data[-1]) and supports slicing, so you can also work with a subset of the records only:

for c in councillors[:5]:
    print(c['LastName'], c['FirstName'])

Use Together with pandas

Convert any result to a pandas DataFrame using .to_dataframe():

import swissparlpy as spp

data = spp.get_data('MemberCouncil', Language='DE', limit=10)
df = data.to_dataframe()
print(df[['LastName', 'FirstName', 'CantonName']])

Or use the classic pandas constructor:

import swissparlpy as spp
import pandas as pd

data = spp.get_data('Party', Language='DE')
df = pd.DataFrame(data)
print(df.shape)
print(df.dtypes)

Large Queries

swissparlpy handles server-side pagination transparently: records are only fetched from the API when you actually access them, so slicing a large result set only downloads the records you need.

import swissparlpy as spp

# Only the count is requested, no records are loaded yet
votes = spp.get_data('Voting', Language='DE', IdSession=5101)
print(votes.count)

# Load the first 100 records only
for vote in votes[:100]:
    print(vote['LastName'], vote['DecisionText'])

Very large queries emit a ResultVeryLargeWarning. During development, slice the result (e.g. votes[:100]) instead of iterating over all records.

Very large tables (especially Voting and Transcript) may still result in server-side errors (500 Internal Server Error). In that case download the data in smaller batches, store the individual blocks and combine them afterwards – see download_votes_in_batches.py:

import swissparlpy as spp
import pandas as pd

# Download the votes of the 50th legislative period session by session
sessions50 = spp.get_data("Session", Language="DE", LegislativePeriodNumber=50)
frames = []
for session in sessions50:
    data = spp.get_data("Voting", Language="DE", IdSession=session['ID'])
    frames.append(data.to_dataframe())

df_voting50 = pd.concat(frames)

OData Backend: Substrings

To query for substrings, suffix the variable name with one of the following operators:

Suffix Description
__startswith The value starts with the given string
__contains The value contains the given string
import swissparlpy as spp

# Find all persons whose last name starts with 'Bal'
persons = spp.get_data("Person", Language="DE", LastName__startswith='Bal')
print(persons.count)
# 12

# Find all business items with 'CO2' in the title
co2_business = spp.get_data("Business", Title__contains="CO2", Language="DE")
print(co2_business.count)
# 265

OData Backend: Date Ranges

To query for date ranges, suffix the variable name with a comparison operator and pass a datetime object:

Suffix Description
__gt greater than
__gte greater than or equal
__lt less than
__lte less than or equal
import swissparlpy as spp
from datetime import datetime

business = spp.get_data(
    "Business",
    Language="DE",
    SubmissionDate__gt=datetime.fromisoformat('2019-09-30'),
    SubmissionDate__lte=datetime.fromisoformat('2019-10-31')
)
print(business.count)
# 22

OData Backend: Advanced Filter

Text query

For complex filter expressions, use the filter keyword with a raw OData filter string. Operators like eq, ne, lt, lte, gt, gte, startswith() and contains are supported:

import swissparlpy as spp

persons = spp.get_data(
    "Person",
    filter="(startswith(FirstName, 'Ste') or LastName eq 'Seiler') and Language eq 'DE'"
)
df = persons.to_dataframe()
print(df[['FirstName', 'LastName']])

Callable filter

The filter keyword also accepts a callable, which allows for more advanced filters. spp.Filter provides the or_ and and_ helpers:

import swissparlpy as spp

# filter by FirstName == 'Stefan' OR LastName == 'Seiler'
def filter_by_name(ent):
    return spp.Filter.or_(
        ent.FirstName == 'Stefan',
        ent.LastName == 'Seiler'
    )

df = spp.get_data("Person", filter=filter_by_name, Language='DE').to_dataframe()
print(df[['FirstName', 'LastName']])

Visualize Voting Results

Requires installation with pip install swissparlpy[visualization].

The plot_voting() function visualizes the voting results of the Swiss National Council according to the seating order. It expects the data of the Voting table of the OData backend.

import swissparlpy as spp
import matplotlib.pyplot as plt

# Get the voting data of one specific vote
votes = spp.get_data("Voting", Language="DE", IdVote=23458)

# Create the visualization with the default scoreboard theme
fig = spp.plot_voting(votes, theme='scoreboard', result=True)
plt.show()

Voting visualization example with scoreboard

The following themes are available:

Theme Description
scoreboard (default) Imitates the council hall scoreboard (neon colors on black background)
sym1, sym2 Colored symbols on light background
poly1, poly2, poly3 Color-filled polygons with different edge styles

Parliamentary groups can be highlighted with the highlight parameter:

fig = spp.plot_voting(
    votes,
    theme='poly1',
    highlight={'ParlGroupCode': ["S"]},
    result=True
)
plt.show()

Voting visualization example with poly1 and a highlighted group

The mapping from seats to persons is currently not historized, so “older” votes might not be displayed correctly. You can provide your own mapping with the seats parameter.


OpenParlData Backend

Search with the OpenParlData Backend

Besides the variables of a table, every query parameter of the OpenParlData API (e.g. limit, offset, sort_by, fields, search, lang) can be passed as a keyword argument. swissparlpy only sets lang_format="flat" itself, all other parameters use the documented API defaults.

import swissparlpy as spp

opd_client = spp.SwissParlClient(backend="openparldata")

# Simple filter by field value
response = opd_client.get_data("persons", firstname="Karin", lastname="Keller-Sutter")
df = response.to_dataframe()
print(df[['firstname', 'lastname', 'title']])
Output
  firstname       lastname                         title
0     Karin  Keller-Sutter  Dipl. Konferenzdolmetscherin

Full-text search:

search only looks at the metadata by default (search_scope="metadata"). Pass search_scope="all" explicitly to search the full-text indexes, e.g. the text of a speech.

import swissparlpy as spp

opd_client = spp.SwissParlClient(backend="openparldata")
response = opd_client.get_data(
    "speeches",
    search_mode="natural",
    search_scope="all",
    search_language="de",
    search="Budget"
)
print(len(response))
df = response.to_dataframe()
print(df[["id", "person_id", "date_start", "text_content_de"]].head())
Output
          id body_key  person_id  meeting_id           date_start date_end                                    text_content_de
0    1100333      351     4256.0        1262  2024-11-14T18:18:52     None  <p><b>Corina Liebi (JGLP)</b> für die PVS: Für...
1    1100301      351     4191.0        1578  2024-05-30T22:24:34     None  <p><b>Ursina Anderegg (GB)</b> für die Fraktio...
2    1100187      351     4139.0        1219  2025-11-20T18:02:10     None  <p><b>Debora Alder-Gasser (EVP)</b> für die Ko...
3    1100167      351     4315.0        1219  2025-11-20T17:11:50     None  <p><b>Simone Richner (FDP)</b> für die Kommiss...
4    1100016      351     4237.0        1628  2024-06-27T13:44:06     None  <p><b>Franziska Geiser (GB)</b> für die FIKO: ...
..       ...      ...        ...         ...                  ...      ...                                                ...
452  1088291      351     4237.0        1193  2025-03-27T21:51:35     None  <p><b>Franziska Geiser (GB)</b> für die Frakti...
453  1088272      351     4162.0        1404  2025-03-20T17:36:23     None  <p><b>Janina Aeberhard (GLP)</b> für die Kommi...
454  1088255      351     4123.0        1870  2023-09-21T15:50:27     None  <p><b>Barbara Keller (SP)</b> für die SBK: Ich...
455  1088206      351     4114.0        1404  2025-03-20T17:50:35     None  <p><b>Laura Curau (Mitte)</b> für die Fraktion...
456  1088186      351     4237.0        1404  2025-03-20T18:39:10     None  <p><b>Franziska Geiser (GB)</b> für die Frakti...

[457 rows x 7 columns]

limit is a Page Size, Not a Cap

limit is passed straight to the API, where it controls the size of a single page (500 by default). The response follows the next_page links transparently, so iterating a result still yields all matching records, no matter which limit you set:

import swissparlpy as spp

opd_client = spp.SwissParlClient(backend="openparldata")
response = opd_client.get_data("persons", limit=10)
print(len(response))        # total number of records, taken from the first page
# 26574
print(len(list(response)))  # iterating loads every page
# 26574

To only get a few records, use get_glimpse() or slice the response (response[:10]).

len(response) is the total record count reported by the API and is available after a single request, which makes it a cheap way to count records.

Case Sensitivity of search_mode="exact"

While the API documents search_mode="exact" as a case-insensitive exact match, it behaves case-sensitively in practice: search="Nationalrat" returns results, while search="nationalrat" does not.

The OpenParlData API returns related entities alongside main records. You can navigate these relationships easily:

import swissparlpy as spp

opd_client = spp.SwissParlClient(backend="openparldata")
geru = opd_client.get_data("persons", firstname="Gerhard", lastname="Andrey")[0]

# List available related tables
print(geru.get_related_tables())
# ['memberships', 'interests', 'access_badges', ...]

# Load a related table as a DataFrame
member_df = geru.get_related_data('memberships').to_dataframe()
print(member_df[["group_name_de", "role_name_de", "type_harmonized"]].head())
Output

                            external_id               group_name_de      role_name_de   type_harmonized
0              CHE_interest_kultur_4245                      Kultur          Mitglied    interest_group
1  936edfe6-f8fd-4667-a986-ab5200acafb9  Gruppe Parlaments-IT (PIT)          Mitglied  committee_ad_hoc
2  6f42fed7-0dc6-4ed7-b655-b391ad828068  Gruppe Parlaments-IT (PIT)          Mitglied  committee_ad_hoc
3  63898798-ac17-469f-bb21-5e562d76b1de  Gruppe Parlaments-IT (PIT)  Vizepräsident/in  committee_ad_hoc
4  28d9ed41-e55c-4c55-a1f3-ab1300c25d52                     Büro NR  Stimmenzähler/in         committee

API Reference

All public module-level functions:

Function Description
spp.get_tables(backend='odata') Return a list of available table names
spp.get_variables(table, backend='odata') Return the variable names of a table
spp.get_overview(backend='odata') Return a dict of all tables and their variables
spp.get_glimpse(table, rows=5, backend='odata') Return the first rows records of a table
spp.get_data(table, filter=None, backend='odata', **kwargs) Fetch data from a table with optional filters
spp.plot_voting(votes, ...) Plot a National Council seat map for a vote (OData Voting data)

The same methods are available on a spp.SwissParlClient instance (without the backend parameter, which is passed to the constructor instead).