The AuthoritySpoke library provides you with Python classes that you can use to represent a limited subset of English statements, so you can create computable annotations representing aspects of legal reasoning and factfinding. In the newly-released version 0.5 of AuthoritySpoke, I’ve redesigned the interface for creating these phrases to use Python template strings, which I think should make the interface more idiomatic and consistent with existing Python programming patterns.

I chose to implement this feature with template strings instead of Python’s more powerful methods for inserting data into text, such as f-strings or the .format() method, because template strings’ relative lack of versatility makes them more predictable and less bug-prone. Template strings don’t execute any code when they run, so they present less of a security problem and they can be used with untrusted user-generated data. Since this is a big change to the interface, I’ll provide a variety of examples of how template strings will work with other AuthoritySpoke classes. These examples can also be found in the documentation, which may be updated more frequently.

Predicate objects

Here’s an example of a template string used to create a Predicate object in AuthoritySpoke version 0.5:

>>> from authorityspoke import Predicate
>>> parent_sentence = Predicate("$mother was ${child}'s parent")

The phrase that we passed to the Predicate constructor is used to create a Python template string. Template strings are part of the Python standard library. The dollar signs and curly brackets are special symbols used to indicate placeholders in Python’s template string syntax.

AuthoritySpoke’s Predicate class is structured like a statement in Predicate logic. The Predicate is like a partial sentence with blank spaces marked by placeholders. The placeholders can be replaced by nouns that become the subjects or objects of this potential sentence. The Predicate class isn’t intended to turn Python into a logic programming environment like Prolog, nor is it designed to be used as an interface to a deep learning language model like GPT-3. Instead, the Predicate class is designed to help you run computations over a curated set of annotations where you can usually guarantee that the same phrasing has been used for the same legal concept.

Here’s an example of how Python lets you replace the placeholders in a template string with new text.

>>> parent_sentence.template.substitute(mother="Ann", child="Bob")
"Ann was Bob's parent"

Don’t worry: the use of the past tense doesn’t indicate that a tragedy has befallen Ann or Bob. The Predicate class is designed to be used only with an English-language phrase in the past tense. The past tense is used because legal analysis is usually backward-looking, determining the legal effect of past acts or past conditions. Don’t use capitalization or end punctuation to signal the beginning or end of the phrase, because the phrase may be used in a context where it’s only part of a longer sentence.

Predicates can be compared using AuthoritySpoke’s .means(), .implies(), and .contradicts() methods. The means method checks whether one Predicate has the same meaning as another Predicate. One reason for comparing Predicates using the means method instead of Python’s == operator is that the means method can still consider Predicates to have the same meaning even if they use different identifiers for their placeholders.

>>> another_parent_sentence = Predicate("$adult was ${kid}'s parent")
>>> parent_sentence.template == another_parent_sentence.template
>>> another_parent_sentence.means(parent_sentence)

You can also add a truth attribute to a Predicate to indicate whether the statement described by the template is considered true or false. AuthoritySpoke can then use that attribute to evaluate relationships between the truth values of different Predicates with the same template text. If you omit a truth parameter when creating a Predicate, the default value is True.

>>> not_parent_sentence = Predicate("$adult was ${kid}'s parent", truth=False)
>>> str(not_parent_sentence)
"it was false that $adult was ${kid}'s parent"
>>> parent_sentence.means(not_parent_sentence)
>>> parent_sentence.contradicts(not_parent_sentence)

In the parent_sentence example above, there are really two different placeholder formats. The first placeholder, mother, is just preceded by a dollar sign. The second placeholder, child, is preceded by a dollar sign and an open curly bracket, and followed by a closed curly bracket. These formats aren’t specific to AuthoritySpoke; they’re part of the Python standard library. The difference is that the format with just the dollar sign can only be used for a placeholder that is surrounded by whitespace. If the placeholder is next to some other character, like an apostrophe, then you need to use the “braced” format with the curly brackets. The placeholders themselves need to be valid Python identifiers, which means they can only be made up of letters, numbers, and underscores, and they can’t start with a number. Docassemble users might already be familiar with these rules, since Docassemble variables also have to be Python identifiers. Check out Docassemble’s documentation for more guidance on creating valid Python identifiers.

Comparison objects

AuthoritySpoke’s Comparison class extends the concept of a Predicate. A Comparison still contains a truth value and a template string, but that template should be used to identify a quantity that will be compared to an expression using a sign such as an equal sign or a greater-than sign. This expression must be a constant: either an integer, a floating point number, a date, or a physical quantity expressed in units that can be parsed using the pint library. To encourage consistent phrasing, the template string in every Comparison object must end with the word “was”. AuthoritySpoke will then build the rest of the phrase using the comparison sign and expression that you provide.

Comparisons with Measurements and Units

To use a measurement as a Comparison’s expression, pass the measurement as a string when constructing the Comparison object, and it will be converted to a pint Quantity.

>>> from authorityspoke import Comparison
>>> drug_comparison = Comparison(
>>>     "the weight of marijuana that $defendant possessed was",
>>>     sign=">=",
>>>     expression="0.5 kilograms")
>>> str(drug_comparison)
'that the weight of marijuana that $defendant possessed was at least 0.5 kilogram'

(The pint library always uses singular nouns for units like “kilogram”, when rendering them as text.)

By making the quantitative part of the phrase explicit, you make it possible for AuthoritySpoke to consider quantities when checking whether one Comparison implies or contradicts another.

>>> smaller_drug_comparison = Comparison(
>>>     "the weight of marijuana that $defendant possessed was",
>>>     sign=">=",
>>>     expression="250 grams")
>>> str(smaller_drug_comparison)
'that the weight of marijuana that $defendant possessed was at least 250 gram'

AuthoritySpoke will understand that if the weight was at least 0.5 kilograms, that implies it was also at least 250 grams.

>>> drug_comparison.implies(smaller_drug_comparison)

If you phrase a Comparison with an inequality sign using truth=False, AuthoritySpoke will silently modify your statement so it can have truth=True with a different sign. In this example, the user’s input indicates that it’s false that the weight of the marijuana was more than 10 grams. AuthoritySpoke interprets this to mean it’s true that the weight was no more than 10 grams.

>>> drug_comparison_with_upper_bound = Comparison(
>>>     "the weight of marijuana that $defendant possessed was",
>>>     sign=">",
>>>     expression="10 grams",
>>>     truth=False)
>>> str(drug_comparison_with_upper_bound)
'that the weight of marijuana that $defendant possessed was no more than 10 gram'

Of course, this Comparison contradicts the other Comparisons that asserted the weight was much greater.

>>> drug_comparison_with_upper_bound.contradicts(drug_comparison)

The unit that the Comparison parses doesn’t have to be weight. It could also be distance, time, volume, units of surface area such as square kilometers or acres, or units that combine multiple dimensions such as miles per hour or meters per second.

Comparisons with Integer and Float Expressions

When the number needed for a Comparison isn’t a physical quantity that can be described with the units in the pint library, you should phrase the text in the template string to explain what the number describes. The template string will still need to end with the word “was”. The value of the expression parameter should be an integer or a floating point number, not a string to be parsed.

>>> three_children = Comparison(
>>>     "the number of children in ${taxpayer}'s household was",
>>>     sign="=",
>>>     expression=3)
>>> str(three_children)
"that the number of children in ${taxpayer}'s household was exactly equal to 3"

The numeric expression will still be available for comparison methods like implies or contradicts, but no unit conversion will be available.

>>> at_least_two_children = Comparison(
>>>     "the number of children in ${taxpayer}'s household was",
>>>     sign=">=",
>>>     expression=2)
>>> three_children.implies(at_least_two_children)

Floating point comparisons work similarly.

>>> specific_tax_rate = Comparison(
>>>     "${taxpayer}'s marginal income tax rate was",
>>>     sign="=",
>>>     expression=.3)
>>> tax_rate_over_25 = Comparison("${taxpayer}'s marginal income tax rate was", sign=">", expression=.25)
>>> specific_tax_rate.implies(tax_rate_over_25)

Comparisons with Dates

The expression field of a Comparison can be a

>>> from datetime import date
>>> copyright_date_range = Comparison("the date when $work was created was", sign=">=", expression = date(1978,1,1))
>>> str(copyright_date_range)
'that the date when $work was created was at least 1978-01-01'

And dates and date ranges can be compared with each other, similar to how numbers can be compared to number ranges.

>>> copyright_date_specific = Comparison("the date when $work was created was", sign="=", expression = date(1980,6,20))
>>> copyright_date_specific.implies(copyright_date_range)

Using Entities as Context Terms

AuthoritySpoke isn’t limited to comparing Predicates and Comparisons containing unassigned placeholder text. You can use Entity objects to assign specific terms to the placeholders. You then link the terms to the Predicate or Comparison inside a Fact object.

>>> from authorityspoke import Entity, Fact
>>> ann = Entity("Ann", generic=False)
>>> claude = Entity("Claude", generic=False)
>>> ann_tax_rate = Fact(specific_tax_rate, terms=ann)
>>> claude_tax_rate = Fact(tax_rate_over_25, terms=claude)
>>> str(ann_tax_rate)
"the fact that Ann's marginal income tax rate was exactly equal to 0.3"
>>> str(claude_tax_rate)
"the fact that Claude's marginal income tax rate was greater than 0.25"

Before, we saw that the Comparison specific_tax_rate implies tax_rate_over_25. But when we have a fact about the tax rate of a specific person named Ann, it doesn’t imply anything about Claude’s tax rate.

>>> ann_tax_rate.implies(claude_tax_rate)

That seems to be the right answer in this case. But sometimes, in legal reasoning, we want to refer to people in a generic sense. We might want to say that a statement about one person can imply a statement about a different person, because most legal rulings can be generalized to apply to many different people regardless of exactly who those people are. To illustrate that idea, let’s create two “generic” people and show that a Fact about one of them implies a Fact about the other.

>>> devon = Entity("Devon", generic=True)
>>> elaine = Entity("Elaine", generic=True)
>>> devon_tax_rate = Fact(specific_tax_rate, terms=devon)
>>> elaine_tax_rate = Fact(tax_rate_over_25, terms=elaine)
>>> devon_tax_rate.implies(elaine_tax_rate)

In the string representations of Facts, generic Entities are shown in angle brackets as a reminder that they may be considered to correspond to different Entities when being compared to other objects.

>>> str(devon_tax_rate)
"the fact that <Devon>'s marginal income tax rate was exactly equal to 0.3"
>>> str(elaine_tax_rate)
"the fact that <Elaine>'s marginal income tax rate was greater than 0.25"

When the implies method produces the answer True, we can also use the explain_implication method to find out which pairs of generic terms can be considered analagous to one another.

>>> explanation = devon_tax_rate.explain_implication(elaine_tax_rate)
>>> str(explanation)
'ContextRegister(<Devon> is like <Elaine>)'

Identical Terms

If for some reason you need to mention the same term more than once in a Predicate or Comparison, use the same placeholder for that term each time. When you provide a sequence of terms for the Fact object using that Predicate, only include each unique term once. The terms should be listed in the same order that they first appear in the template text.

>>> opened_account = Fact(
>>>     Predicate("$applicant opened a bank account for $applicant and $cosigner"),
>>>     terms=(devon, elaine))
>>> str(opened_account)
'the fact that <Devon> opened a bank account for <Devon> and <Elaine>'

Interchangeable Terms

Sometimes, a Predicate or Comparison needs to mention two terms that are different from each other, but that have interchangeable positions in that particular phrase. To convey interchangeability, the template string should use identical text for the placeholders for the interchangeable terms, except that the different placeholders should each end with a different digit.

>>> ann = Entity("Ann", generic=False)
>>> bob = Entity("Bob", generic=False)
>>> same_family = Predicate(
>>>     "$relative1 and $relative2 both were members of the same family")
>>> ann_and_bob_were_family = Fact(
>>>     same_family,
>>>     terms=(ann, bob))
>>> str(ann_and_bob_were_family)
'the fact that Ann and Bob both were members of the same family'
>>> bob_and_ann_were_family = Fact(
>>>     same_family,
>>>     terms=(bob, ann))
>>> str(bob_and_ann_were_family)
'the fact that Bob and Ann both were members of the same family'
>>> ann_and_bob_were_family.means(bob_and_ann_were_family)

If you choose placeholders that don’t fit the pattern of being identical except for a final digit, then transposing two non-generic terms will change the meaning of the Fact.

>>> parent_sentence = Predicate("$mother was ${child}'s parent")
>>> ann_is_parent = Fact(parent_sentence, terms = (ann, bob))
>>> bob_is_parent = Fact(parent_sentence, terms = (bob, ann))
>>> str(ann_is_parent)
"the fact that Ann was Bob's parent"
>>> str(bob_is_parent)
"the fact that Bob was Ann's parent"
>>> ann_is_parent.means(bob_is_parent)

Higher-Order Predicates

In AuthoritySpoke, terms referenced by a Predicate or Comparison can contain references to Facts as well as Entities. That mean they can include the text of other Predicates. This feature is intended for incorporating references to what people said, knew, or believed.

>>> statement = Predicate("$speaker told $listener $event")
>>> bob_had_drugs = Fact(smaller_drug_comparison, terms=bob)
>>> bob_told_ann_about_drugs = Fact(statement, terms=(bob, ann, bob_had_drugs))
>>> str(bob_told_ann_about_drugs)
'the fact that Bob told Ann the fact that the weight of marijuana that Bob possessed was at least 250 gram'

A higher-order Predicate can be used to establish that one Fact implies another. In legal reasoning, it’s common to accept that if a person knew or communicated something, then the person also knew or communicated any facts that are obviously implied by what the person actually knew or said. In this example, the fact that Bob told Ann he possessed more than 0.5 kilograms means he also told Ann that he possessed more than 250 grams.

>>> bob_had_more_drugs = Fact(drug_comparison, terms=bob)
>>> bob_told_ann_about_more_drugs = Fact(
>>>     statement,
>>>     terms=(bob, ann, bob_had_more_drugs))
>>> str(bob_told_ann_about_more_drugs)
'the fact that Bob told Ann the fact that the weight of marijuana that Bob possessed was at least 0.5 kilogram'
>>> bob_told_ann_about_more_drugs.implies(bob_told_ann_about_drugs)

However, a contradiction between Facts referenced in higher-order Predicates doesn’t cause the first-order Facts to contradict one another. It’s not contradictory to say that a person has said two contradictory things.

>>> bob_had_less_drugs = Fact(drug_comparison_with_upper_bound, terms=bob)
>>> bob_told_ann_about_less_drugs = Fact(
>>>     statement,
>>>     terms=(bob, ann, bob_had_less_drugs))
>>> str(bob_told_ann_about_less_drugs)
'the fact that Bob told Ann the fact that the weight of marijuana that Bob possessed was no more than 10 gram'
>>> bob_told_ann_about_less_drugs.contradicts(bob_told_ann_about_more_drugs)

Higher-order Facts can refer to terms that weren’t referenced by the first-order Fact. AuthoritySpoke will recognize that the use of different terms in the second-order Fact changes the meaning of the first-order Fact.

>>> claude_had_drugs = Fact(smaller_drug_comparison, terms=claude)
>>> bob_told_ann_about_claude = Fact(
>>>     statement,
>>>     terms=(bob, ann, claude_had_drugs))
>>> str(bob_told_ann_about_claude)
'the fact that Bob told Ann the fact that the weight of marijuana that Claude possessed was at least 250 gram'
>>> bob_told_ann_about_drugs.implies(bob_told_ann_about_claude)

More Information

Visit the AuthoritySpoke documentation for more information including instructions for installing AuthoritySpoke from the Python Package Index. Please open an issue in AuthoritySpoke’s GitHub repo if you have any problems, including any problems using the documentation. On Twitter, follow me at @mcareyaus, or follow @authorityspoke for project updates.