Advanced User Guide¶
The tutorial shows how to build a study. This guide describes how CourtPy works and everything you can configure.
How the pieces fit together¶
CourtListener ──BulkData / CourtListener──▶ saved cases ──read──▶ Case ──Parser──▶ case table ──coders──▶ amos.Dataset ──amos.Project──▶ results
Lexis-Nexis ──lexis.split──────────────▶ (JSON or text) (sections (rulebooks) (one row (parties, (models, tables,
of text) per case) outcomes) figures, report)
In a project, a loader (such as load_court_listener) does every step before
amos.Dataset as the first technique of the wrangler.
| Module | Contents |
|---|---|
courtpy.bulk |
BulkData, which extracts cases from CourtListener's bulk data. |
courtpy.courtlistener |
CourtListener, a client for the REST API, and read_case, which reads a saved CourtListener case. |
courtpy.lexis |
split and read_case for Lexis-Nexis text files. |
courtpy.rules |
Rule and Rulebook, which apply the CSV files of rules. |
courtpy.parsers |
Parser and parse, which turn saved cases into a table. |
courtpy.coders |
The coders, which are amos techniques, and code, which applies them. |
courtpy.judges |
Roster, which matches the names in opinions to judges, and the merge_judges, code_judges, and judge_votes techniques. |
courtpy.politics |
build_table, which makes a table of each year's political context from the sources' current files, and the code_politics technique. |
courtpy.scdb |
cases and votes, which load the Supreme Court Database, and the code_scdb technique. |
courtpy.loaders |
The loaders, which are amos techniques that collect, parse, and code cases in a project. |
courtpy.cases |
Case, and save_cases and load_cases for case tables. |
courtpy.secrets |
Storing and finding the CourtListener API key. |
courtpy.options |
Defaults that a project can change before they are used. |
Saved cases¶
Both ways of collecting CourtListener cases save each case as a JSON file named for its cluster id, in a folder named for its court. Its keys are:
| Key | Contents |
|---|---|
source |
"court_listener". |
court |
The court's id, full_name, short_name, citation_string, and jurisdiction. |
cluster |
CourtListener's opinion cluster: the case name, date filed, judges, attorneys, disposition, precedential status, and so on. |
docket |
The docket (with the docket number and date argued), from the bulk data, or from the API if "dockets" was asked for. Otherwise null. |
citations |
The case's citations as text, such as "950 F.3d 12". |
opinions |
Each opinion: its type (such as "010combined" or "040dissent"), author, and one field of text (the first of options._TEXT_FIELDS that is not blank, which CourtListener recommends be "html_with_citations"). |
bulk_date |
For cases from the bulk data, the date of the files. |
Lexis-Nexis cases are text files, one for each case. courtpy.lexis.split
divides Lexis-Nexis downloads of many cases into such files and removes
clutter such as star paging.
Parsing¶
courtpy.parse (or a Parser) turns each saved case into a row in three
stages:
- A reader makes a
Casewith sections of text andmetadata. CourtListener sections come from its data (see the table of sections in Writing Rules); a Lexis-Nexis case has one section, "text". - For Lexis-Nexis, the "lexis_nexis" rules divide the text into the header and the opinions and find each part of the header.
- Every section except those whose names end in "_lines" is collapsed onto one line, and the rules of the jurisdiction ("federal" by default) are applied.
The row starts with the case's metadata, which is used instead of anything
the rules find with the same name (unless the metadata is missing). A few
columns are then added: author (the first of authors), panel_size,
concurrences, dissents, and word_count. The text of the opinions is not
kept unless keep_text is true, because it would make the table very large.
Parsing a large set of cases takes a while. Pass workers (the number of
processes) to parse to use more of your computer.
The case table¶
The table is labeled by "case_id": the CourtListener cluster id, or the name of
the Lexis-Nexis file. Flags are booleans, counts are integers, and matches and
names are lists. Columns of booleans or whole numbers that are missing for
some cases (such as published or year) have the "boolean" and "Int64"
types, which allow missing values.
save_cases saves a table as a CSV file (with lists written as text, separated
by "; ", and the names of those columns saved beside the file) or as a parquet
file (with the optional pyarrow package, which keeps lists and types as they
are), and load_cases loads it again.
Coders¶
The coders are amos techniques, so they can be named in the settings of any
amos worker, and they record what they did in the dataset's history.
code_parties, code_case_type, and code_outcome are mungers (amos.Munger),
which change and add columns without adding or removing rows, so the history
lists the columns that each one changed and created. drop_text is a cleaner
(amos.Cleaner), which removes columns. They use the columns made by the
federal rules.
| Coder | Adds |
|---|---|
code_parties |
Completes the parties' roles from their opposites (if one party is an appellant, the other is an appellee), and adds party1_appealing, party1_defending, and the same for party2. |
code_case_type |
type_criminal (the United States is a party and there is a sign of a criminal case), and each party's role in the case: party1_prosecution, party1_criminal_defendant, party1_civil_plaintiff, and party1_civil_defendant (and the same for party2). |
code_outcome |
outcome_reversal (from the header's disposition, or else from the decision that the opinion states, or else from the opinion's words anywhere), outcome_party1_won and outcome_party2_won, the winner by side (outcome_criminal_defendant_won, outcome_prosecution_won, outcome_civil_plaintiff_won, and outcome_civil_defendant_won), and appeal_by_defendant. Outcomes that cannot be known are missing. |
drop_text |
Removes columns of text and lists (such as names and dates), which models cannot use. Its keep parameter names columns to keep. |
To write your own, subclass amos.Munger and write munge (or subclass
amos.Cleaner and write clean for a coder that removes rows or columns,
amos.Merger and write match for one that adds the columns of another
table, or amos.Shaper and write shape for one that changes what a row
is). It is added to the library as soon as it is defined, so it can be named
in settings:
import dataclasses
import amos
@dataclasses.dataclass
class CodeLongOpinion(amos.Munger):
"""Flags opinions longer than a number of words."""
def munge(self, data, words = 5000, **kwargs):
data["long_opinion"] = data["word_count"] > words
return data
Judges and panels¶
The federal rules find the names of each case's judges as the opinion writes
them (panel_judges, authors, concurring, and dissenting), such as
"LYNCH". courtpy.judges matches those names to people, with a roster made
from the Federal Judicial Center's
Biographical Directory of Article III Federal Judges:
from_fjc downloads three files of a few megabytes (federal judicial service,
demographics, and professional careers) unless they were downloaded before
(update = True downloads them again, for the judges and changes since). The
roster is a table with one row for each court that a judge has served on, with
the president who made the appointment and that president's party, the
American Bar Association's rating, the Senate's vote, the judge's sex, race or
ethnicity, and year of birth, and flags for the judge's career (see
courtpy.judges.build_roster for every column). save writes it as
"roster.csv" in "judges" in CourtPy's data folder, outside of any project,
where the techniques below look for it. Their roster parameter names another.
The roster is a CSV file, so you can change it in a spreadsheet:
- Add a judge, or another name that opinions use for a judge (as "First
Middle Last" in the
aliasescolumn, with semicolons between names). - Add a column of your own, such as a score of each judge's ideology (or pass
scores, a table with the judges'nidnumbers, tofrom_fjc). Every column of numbers or booleans that does not identify or date a judge's service is an attribute of the judge, which the techniques use. - Change how careers are coded with rules of your own (the
rulebookargument). The built-in rules are ininstructions/judges.csv.
Opinions usually give only a judge's last name, so a name is matched with the
court and year of its case: to a judge of the case's court, or else to a judge
of a district court in its circuit (who may sit by designation), or else to a
judge of any other court (who may be visiting). Only judges who were serving
that year, or had left their court no more than two years before
(options._JUDGE_GRACE), are considered. Rather than guess, a name that fits
more than one such judge is not matched (which is why opinions add initials,
as in "M. SMITH" and "N.R. SMITH"), and neither is a name on a case of a court
that has no judges in the roster, such as a bankruptcy appellate panel.
Cases and judges are different things: a case table has a row for each case,
and a roster a row for each court that each judge has served on. amos (0.2.6
and later) joins such tables with
mergers, which
add the columns of another table to the rows that they match without ever
adding or losing a row, and
shapers,
which change what a row is. CourtPy's three techniques for judges are built
from them:
| Technique | Adds |
|---|---|
merge_judges |
A merger. Each row names one judge (in the column that its name parameter names, which is "judge" unless another is set), and gets the columns of that judge's row of the roster, with "judge_" (or another prefix) before their names: every attribute, and age, senior, designated (sitting on a court other than the judge's own), and district_judge, which depend on the case's year and court. All are numbers (1 and 0 for true and false). Its columns parameter names the columns to add, which can be any others of the roster too (such as judge, the judge's name, or president), and indicator names a column that says whether each judge was found. |
code_judges |
A munger. panel_names (the judges who were found, as the roster names them), panel_found (how many, to compare with panel_size), author_name, concurring_names, and dissenting_names, and the composition of the panel: panel_{attribute} is the mean of each attribute among its judges. So panel_woman is the share who are women, and panel_party is the mean party of the presidents who appointed them (from -1, all Democrats, to 1, all Republicans). panel_age, panel_senior, panel_designated, and panel_district_judge depend on the case's year and court. |
judge_votes |
A shaper. Replaces the table of cases with one row for each judge on each case, labeled by "vote_id" (such as "4567890-2"): the case's columns, case_id, judge, judge_nid, judge_{attribute}, colleagues_{attribute} (the mean among the other judges of the panel), judge_author, judge_concurred, judge_dissented, and for each outcome of the case, vote_{outcome}: the outcome, or its opposite if the judge dissented. So vote_reversal is whether the judge voted to reverse. Its outcomes parameter names the outcomes (by default, every column that starts with "outcome_"). Like every shaper, it takes label, task, and groups for the table that it makes, and must come before the data is split. |
code_judges and judge_votes do what a wrangler of
lists_to_rows, merge_judges, group_rows would do: they make a row for each
name that a case lists, find the judge that each name refers to, and (for
code_judges) summarize the rows of each case. Use merge_judges itself
for a table that names one judge in each row. For example, to add what is
known about the author of each opinion to a table of cases:
[wrangler]
techniques = load_court_listener, merge_judges, drop_text
[merge_judges_parameters]
name = author
prefix = author_
indicator = author_found
The dataset's history records how many rows a merger matched, which is worth checking (and reporting) every time:
print(project.result.history[-2])
# {'technique': 'merge_judges', 'source': '...roster.csv', 'rows': 5212, 'matched': 4980, 'created': ['author_party', ...]}
All three can be among a loader's "coders", so a vote can be the label of a
study (see examples/judge_votes.ini
and the recipes). In Python, courtpy.judges.vote_table makes
the same table as judge_votes, and a Roster matches one name at a time:
row = roster.match("LYNCH", court_num = 1, year = 2020)
print(roster.names([row]), roster.describe(row, court_num = 1, year = 2020))
A loader passes its coders no parameters, so as coders they use the saved
roster. To name another, list them as techniques of the wrangler instead
(after the loader), with a section of parameters for each. merge_judges
takes the roster as its source, as every merger takes its other table, and
finds a file that is named there as the project's clerk finds files: in the
current folder, and then in the input folder. code_judges and judge_votes
take it as roster, whose path is relative to the folder that the study is
run from.
Political context¶
code_politics adds what a table of years says about each case's year, with
"politics_" before the names of the table's columns. With no table, it adds
politics_president_party: -1 for a Democratic president and 1 for a
Republican. courtpy.politics.build_table makes a table with two more
sources:
| Columns | Meaning | File |
|---|---|---|
supreme_court_median, supreme_court_min, supreme_court_max |
The Martin-Quinn scores of the median justice and of the most liberal and most conservative justices in each term. | "court.csv", the scores of the court in each term. |
senate_median, house_median |
The median NOMINATE score of the members of each chamber of Congress. | "HSall_members.csv", the ideology of every member of every Congress (Voteview's "Member Ideology" data). |
Higher scores are more conservative. build_table downloads the sources'
current files (about 6 megabytes) the first time, and keeps them in
"politics" in CourtPy's data folder.
courtpy.politics.download(overwrite = True) downloads them again, for the
terms and votes since. To use files of your own instead, pass their paths,
or False to leave a source out:
[files]
input_folder = data
[wrangler]
techniques = load_court_listener, code_politics, drop_text
[code_politics_parameters]
source = politics.csv
code_politics is the merge_keys merger of amos with the year as its key,
so it takes that merger's parameters. source is the table of years, which
is found as the project's clerk finds files (in the current folder, and then
in the input folder). Any table with a "year" column can be used, so other
measures can be added in a spreadsheet. on and other_on name the columns
to match by, prefix the text before the new columns' names, columns the
columns to add, and indicator a column that says whether each case's year
was in the table. A table with two rows for a year stops the study, unless
duplicates says to use the "first" or "last" of them. As one of a loader's
coders, code_politics gets no parameters, so it adds only the party of the
president.
courtpy.politics.justices returns the Martin-Quinn score of each justice in
each term, from the "justices.csv" file of the same page, for studies of the
Supreme Court (see below).
The Supreme Court Database¶
The Supreme Court Database codes every case that
the Supreme Court has decided since its 1946 term: the issue, the direction
of the decision, what the court did with the decision below, which party won,
and how each justice voted. code_scdb (a merger) adds its coding to parsed
cases of the Supreme Court, with "scdb_" before the database's names for its
columns:
[load_court_listener_parameters]
courts = scotus
coders = code_parties, code_case_type, code_outcome, code_scdb
A case is matched by the database's id for it, which CourtListener records
for cases of the Supreme Court (the scdb_id column), or else by one of its
citations (the citation column), such as "347 U.S. 483". Rather than guess,
a case whose citations are those of more than one case of the database (as
short orders on one page of a reporter are) is not matched. The history
records how many cases were matched and the release of the database that was
used.
The latest release is downloaded from the database's
data page the first time, and kept in "scdb"
in CourtPy's data folder (courtpy.scdb.download(overwrite = True) looks for
a newer one). As a technique of the wrangler, code_scdb takes a merger's
parameters: source (a file of the database that you downloaded yourself),
columns (such as "issueArea, decisionDirection, partyWinning"), prefix,
and indicator, and on, other_on, and citations for the columns to
match by. The database's
online codebook explains each
variable.
In Python, courtpy.scdb.cases returns the database's table with a row for
each case, and courtpy.scdb.votes its table with a row for each justice in
each case. The Martin-Quinn scores use the database's numbers for the
justices, so a merger adds each justice's score to each vote:
import amos
import courtpy
votes = amos.mergers.MergeKeys().apply(
courtpy.scdb.votes(),
source = courtpy.politics.justices(),
on = ["term", "justice"])
print(votes.data[["caseName", "justiceName", "vote", "martin_quinn"]].head())
print(votes.history[-1]["matched"])
Loaders¶
amos (0.2.3 and later) loads a project's data with loaders, the first
techniques of the wrangler, so a project with a loader needs no item.
CourtPy's loaders are a genre of amos loaders, CaseLoader:
| Loader | Source | Does |
|---|---|---|
load_court_listener |
A folder of saved CourtListener cases. Defaults to "court_listener". | Downloads cases into the folder (if "download" is set), parses them, and codes them. |
load_lexis_nexis |
A folder of Lexis-Nexis cases, one per text file. Defaults to "lexis_nexis". | Divides any "batches" into the folder, parses the cases, and codes them. |
load_cases |
A table saved by save_cases (or courtpy parse). |
Loads the table, with its lists and types. It applies no coders unless "coders" names some. |
Their parameters go in a "{loader}_parameters" section of the settings:
| Parameter | Loaders | Meaning |
|---|---|---|
source |
all | The folder (or, for load_cases, the file) to load. |
coders |
all | Techniques to apply after parsing. Defaults to "code_parties, code_case_type, code_outcome" ("none" for load_cases). Use "none" for none. |
save |
all | File to save the coded table to (".csv" or ".parquet"). |
reuse |
all | Whether to load the saved table, if there is one, instead of collecting, parsing, and coding again. |
label, task, groups |
all | As in amos. They default to those in the "general" section. |
download |
load_court_listener |
"bulk" to extract cases from the bulk data, "api" to download them with the API, or "none" (the default) to use the cases already in the folder. |
courts |
load_court_listener |
CourtListener court ids (such as "ca1, ca2") or groups (federal_appellate, federal_circuits, or supreme_court). |
start_date, end_date |
load_court_listener |
Dates filed (YYYY-MM-DD). |
max_cases, overwrite |
load_court_listener |
Most cases to download, and whether to download cases saved before. |
dockets |
load_court_listener |
For "api", whether to download each case's docket too (one more request per case). |
bulk_folder, bulk_date, stream |
load_court_listener |
For "bulk", where to keep the bulk files, which date's files to use, and whether to read them from CourtListener without saving them. |
batches |
load_lexis_nexis |
Files (or folders) of many cases to divide into the source folder first. |
rulebooks |
load_court_listener, load_lexis_nexis |
Rules to parse with: built-in names (such as "federal") or CSV files or folders. Defaults to "federal". |
keep_text, limit, workers |
load_court_listener, load_lexis_nexis |
Whether to keep the opinions' text, most cases to parse, and processes to parse with. |
The loaders work through the project's clerk, as amos loaders do. A folder
or file named by a relative path is looked for in the current folder and
then in the clerk's input folder (the "input_folder" of the "files" section),
where downloads save their cases, and "save" is in the clerk's interim folder
("interim_folder"). The label is checked after the coders run, so it can be a
column that a coder makes (such as outcome_reversal), and the dataset's
history records the loader and then each coder.
A loader can also be used without a project. apply returns an
amos.Dataset:
dataset = courtpy.loaders.LoadCourtListener().apply(
source = "court_listener", label = "outcome_reversal")
The CourtListener API¶
A free CourtListener account can make 5 requests a minute, 50 an hour, and 125
a day (as of 2026); members have more. CourtListener asks for 20 cases (one
page of opinion clusters) in one request and their opinions in one more, so a
free account can download roughly a thousand cases a day.
- Requests are at least
min_intervalseconds apart (1 by default). - When CourtListener asks for a pause (an HTTP 429 response), the client waits
as long as it is asked, up to
max_waitseconds (an hour by default), which rides out the minute and hour limits. A longer pause (the daily limit) raises aRateLimitError. - The place in each court's list of cases is saved in a ".courtpy" folder inside the download folder, so running the same download again resumes it. Running a finished download again takes one request for each court and saves any cases added since.
download_casedownloads one case by its cluster id.
The bulk data¶
CourtListener publishes its whole database every three months, at
https://com-courtlistener-storage.s3-us-west-2.amazonaws.com/list.html?prefix=bulk-data/.
BulkData.extract reads five files, in this order: courts, dockets (about 5 GB),
opinion clusters (about 2.5 GB), citations, and opinions (about 55 GB). It keeps
only the cases of the courts and dates asked for, so it needs little memory,
but reading the opinions file takes hours. Extract every court and year you may
need at once.
By default, the files are downloaded (resuming an interrupted download) and
kept in "bulk" in CourtPy's data folder, outside of any project, so they can
be used again. Set folder to keep them elsewhere, date to use older files
(available() lists the dates), or stream = True to read them from
CourtListener without saving them.
Secrets and folders¶
courtpy.secrets.get_api_key looks for the API key in, in order: the
COURTLISTENER_API_KEY environment variable, the system keyring, the secrets
file (courtpy.secrets.secrets_path()), and a .env file in the current
folder. set_api_key stores the key in the keyring if there is one and in the
secrets file otherwise (or where its store argument says).
| Folder | Holds | Default | Changed with |
|---|---|---|---|
| Configuration | The secrets file. | "courtpy" in %APPDATA% (Windows) or ~/.config |
COURTPY_CONFIG_DIR |
| Data | Bulk data files, the roster of judges and the files it is made from, and the files of Martin-Quinn scores, NOMINATE scores, and the Supreme Court Database. | "courtpy" in %LOCALAPPDATA% (Windows) or ~/.local/share |
COURTPY_DATA_DIR |
Options¶
courtpy.options holds defaults as module-level constants, which a project can
change before they are used. For example, to wait at least two seconds between
requests to the API, or to add a group of courts:
import courtpy
courtpy.options._MIN_INTERVAL = 2.0
courtpy.options._COURT_GROUPS["new_england"] = ("ca1", "mad", "nhd", "med", "rid")
Errors you may see¶
| Error | Cause |
|---|---|
MissingAPIKeyError: no CourtListener API key was found |
The API needs a key. Run courtpy key set, or use the bulk data. |
CourtListenerError: CourtListener rejected the request (401) |
The key is wrong. Check it with courtpy key show. |
RateLimitError: CourtListener asked courtpy to wait ... hours |
The account's daily limit was reached. Run the same download again later. |
ValueError: ... row N: the pattern ... is not valid |
A rule's regular expression has an error. courtpy rules file.csv checks a file. |
KeyError: 'code_outcome' needs the columns [...] |
A coder needs columns made by the federal rules. |
ValueError: no court_listener cases were found in ... |
The loader's source folder has no saved cases. Set "download" or "source". |
ModuleNotFoundError: No module named 'statsmodels' |
logit_sm (and the other statsmodels models) need statsmodels. Use logit for scikit-learn's logistic regression, or pip install amos[statistics]. |
KeyError: the label 'outcome_reversal' is not a column of the data |
The label is made by a coder that was not run. Check the "coders" setting. |
FileNotFoundError: there is no roster of judges at ... |
merge_judges, code_judges, and judge_votes need a roster. Make and save one with courtpy.judges.Roster.from_fjc().save(), or name one with the "source" parameter of merge_judges or the "roster" parameter of the others. |
ValueError: ... rows of the other table have the same ['year'] as another of its rows |
The table of code_politics has more than one row for a year, and a case can match only one. Set its "duplicates" to "first" or "last", or remove the rows. |
TypeError: 'code_politics' cannot match 'year', which is text in the data, with 'year', which is numbers in the other table |
The cases' years are not numbers (as they are in a table that was saved and loaded without courtpy.load_cases). Load the table with load_cases, or change the column with a munger such as parse_numbers. |
KeyError: 'code_scdb' needs the column 'scdb_id' ... or 'citation' |
code_scdb matches cases by the Supreme Court Database's ids, which CourtListener records for cases of the Supreme Court, or by their citations. Set its "on" or "citations" to the columns that have them. |
ValueError: '...' changes the rows of the data, so it must come before the data is split |
judge_votes is a shaper, which came after a splitter. Put it in the wrangler (or among a loader's coders). |
ImportError: cannot import 'fairlearn.metrics...' |
A study has "groups" (such as case_id, for judges' votes), so the scorecard compares the model across them, which needs fairlearn. Name the scorecard's "metrics", or pip install amos[fairness]. |