“Two Fer” (One) strings and functions discussion for exercism.org solution [Spoiler Warning]

Here are the instructions for the exercise / task I am working on:

Two-fer or 2-fer is short for two for one. One for you and one for me. For this task, given a name, return a string with the message:

One for <name>, one for me.

…where “name” is the given name. However, if the name is missing, return the string:

One for you, one for me.

When the variable name contains strings such as “Bob” or “Alice”, when the unit test runs, the function I wrote returns:

One for Bob, one for me.
One for Alice, one for me.

Since my script produces this output, I pass 2 of 3 unit tests. They flash green. However the failed unit test shows a traceback in my shell indicating a TypeError: two_fer() missing 1 required positional argument: 'name'. When I see this Type Error, I figure it is telling me to create a condition that when an argument of None or an empty string is passed in, the consequent (a string of One for you, one for me.) should be triggered. I feel that the latest iteration of my script accounts for that condition but evidently I’m still not doing something right.

The class method in the unit test includes an assertion where if no name is given: self.assertEqual(two_fer(), "One for you, one for me."). However if I sneakily insert an empty string or None into that line so it says two_fer(“”), then my script passes. But I realize that is kind of cheating.

Could someone shed some more light or meaning onto this TypeError I am encountering?

Below are three code snippets: (a) the latest iteration of my script, (b) the unit test, (c) the pytest traceback in my shell.

Script:

def two_fer(name):
   if name:
       return f"One for {name}, one for me."
   else:
       return f"One for you, one for me."

Full unit test:

import unittest
 
from two_fer import (
   two_fer,
)
 
# Tests adapted from `problem-specifications//canonical-data.json`
 
 
class TwoFerTest(unittest.TestCase):
   def test_no_name_given(self):
       self.assertEqual(two_fer(), "One for you, one for me.")
 
   def test_a_name_given(self):
       self.assertEqual(two_fer("Alice"), "One for Alice, one for me.")
 
   def test_another_name_given(self):
       self.assertEqual(two_fer("Bob"), "One for Bob, one for me.")
 
 
if __name__ == "__main__":
   unittest.main()
 

pytest command traceback:

$ python -m pytest two_fer_test.py
================test session starts ===========
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /home/gnull/dev/projects/python/2018-and-2020/exercism-v3/python/two-fer
collected 3 items                                                                                                                                                                                                                                                                                                                                                                                                 

two_fer_test.py ..F                                                                                                                                                                                                                                                                                                                                                                                         [100%]

==================FAILURES ===================
________ TwoFerTest.test_no_name_given __________

self = <two_fer_test.TwoFerTest testMethod=test_no_name_given>

    def test_no_name_given(self):
>       self.assertEqual(two_fer(), "One for you, one for me.")
E       TypeError: two_fer() missing 1 required positional argument: 'name'

two_fer_test.py:12: TypeError
==================== short test summary info ===================
FAILED two_fer_test.py::TwoFerTest::test_no_name_given - TypeError: two_fer() missing 1 required positional argument: 'name'

It’s also worth mentioning that experimenting with this script below, the pytest locally passes all three assertions but when I submit it to the remote exercism .io platform, it’s still failing the assertion where there is no parameter passed into the function. If you notice in my code snippet below, the name variable at line 4 is assessed by Python for equality with the == operator. When I change it to the assignment operator =, the script completely breaks with an invalid syntax error pointing directly to that line. Would someone care to comment on what is going on here?

If I switch it back to equality == the script continues to pass the unit tests locally but the remote exercism .io test still shows “Failed”.

def two_fer(name=None):
   if name:
       return f"One for {name}, one for me."
   if name==None:
       return f"One for you, one for me."

Give your two_fer function a default value.

def two_fer(name=''):

    ...

Then when you call two_fer('Alice'), the name will be Alice. But if

you pass no name at all, two_fer(), then the name will be set to the

default, which is the empty string.

Hi Steven! Thanks for your reply.

if you pass no name at all, two_fer() , then the name will be set to the default, which is the empty string.

I already tried something similar to this and it worked. To quote myself:

The above code passes all the unit tests with flying colors, just like yours. There is more than one way to accomplish the same result. You used an empty string default argument, '' and I used None. Everything shows a green locally for my script and for yours. But when I submit the code to the remote platform of exercism .org, they all fail. I guess my next step will be to take it up with them.

What do the exercism .org failure message say?

Online code checkers are often substandard. They often only support a
restricted set of Python features and often expect your code to be
word-for-word identical to some template. Knowing nothing about the
exercism website, it’s hard for me to guess why it is failing.

What should be returned if the given name is “you”?

This code is mostly correct, aside from not handling a notable edge case:

I suspect the issue may relate to said edge case, which was not captured by your tests, which is what should happen if an empty string is passed. In this case, it isn’t truthy (bool("") == False), but neither is it None. Thus, your function implicitly returns None, which is very likely not what you want to do.

Exactly what you should do is not clearly and unambigously specified in the problem statement provided above, but you could either treat it as a valid name, or (more likely what you want), also treat it as “missing”.

For the former case, instead of checking for if name is truthy, you can check for if it is None, handling that case specially, and otherwise just injecting the name into the phrase:

def two_fer(name=None):
   if name is None:
       return f"One for you, one for me."
   else:
       return f"One for {name}, one for me."

or more concisely, you can just set a default value for name,

def two_fer(name=None):
   name = "you" if name is None else name
   return f"One for {name}, one for me."

If the latter is the case, as is probably more likely, you can simply change your second if check to an else to capture both cases (or simply unindent it to the top level, since the return obviates the need for the else branch):

def two_fer(name=None):
   if name:
       return f"One for you, one for me."
   else:
       return f"One for {name}, one for me."

or again, more concisely:

def two_fer(name=None):
   name = name or "you"
   return f"One for {name}, one for me."

Either way, I highly suggest adding a case for "" in your unit tests. Also, while you are using pytest as your runner, you are using legacy Unittest structured tests. If that’s all the site supports, that’s unfortunate, but you can simply your tests if you write them in the simpler pytest format, and use parameterization to avoid duplication. Your entire test suite, plus a couple more important cases for good measure, is equivalent to this:

import pytest
from two_fer import two_fer

def test_two_fer_no_input():
    assert two_fer() == "One for you, one for me."

@pytest.mark.parametrize(
    "test_input, name", [(None, "you"), ("", "you"), ("Alice", "Alice"), ("Bob", "Bob")])
def test_two_fer_input(test_input, name):
    assert two_fer(test_input) == f"One for {name}, one for me."

Finally, on a minor note, None (being a singleton, an object that is always the same identical object across a Python session, not simply a different object that compares equal) should always be compared by identity (if name is None), not equality (if name == None). This is mostly a matter of style and convention, but it helps keep your code clearer and more consistent.

Cheers!

Even more concise and idiomatic could be:

name = name or 'you'

From Python documentation:

The expression x or y first evaluates x ; if x is true, its value is returned; otherwise, y is evaluated and the resulting value is returned.

Note that neither and nor or restrict the value and type they return to False and True , but rather return the last evaluated argument. This is sometimes useful, e.g., if s is a string that should be replaced by a default value if it is empty, the expression s or 'foo' yields the desired value. Because not has to create a new value, it returns a boolean value regardless of the type of its argument (for example, not 'foo' produces False rather than '' .)

1 Like

Thanks, I keep forgetting that works—which is a pity, since its such a useful little idiom.

The author of the exercise probably intended for the given name to be a string, but that is not explicitly stated. The following passes all tests, and enables just about anything, including an empty string or None, to be passed as an argument, and to be included in the output:

def two_fer(*args):
    return f"One for {args[0] if args else 'you'}, one for me."

Let’s pass some genuine as well as some silly arguments:

print(two_fer()) # no name given.
print(two_fer("Monty")) # genuine name given as a string.
print(two_fer("")) # an empty string can serve as a name.
print(two_fer("this line,\nand of course")) # a name with a newline.
print(two_fer(42)) # a number can serve as a name.
print(two_fer("you")) # a third person named "you"
print(two_fer("me")) # a third person named "me"
print(two_fer(None)) # None can serve as a name.
One for you, one for me.
One for Monty, one for me.
One for , one for me.
One for this line,
and of course, one for me.
One for 42, one for me.
One for you, one for me.
One for me, one for me.
One for None, one for me.
1 Like

While even more concise, I’d personally say using *args detracts from clarity and safety to a much greater degree, since it means if the caller accidentally passes more than one arg, the rest all get silently discarded rather than resulting in an explicit error, while being confusing to consumers what args is supposed to be.

def two_fer(name=""):
   name = name or "you"
   return f"One for {name}, one for me."

has the exact same number of characters (78) but is significantly clearer and safer for callers and more explicit and easier to understand for later readers.

The intent of this one was, primarily for fun, to enable an empty string to be included among the varieties of given names that could be included within the returned string. But, yes, there is the problem of silence regarding extraneous arguments.

If we execute this …

print(two_fer("")) # an empty string can serve as a name.

… the output is this …

One for , one for me.

As intended, there’s the empty string between the space and the comma.

With this one, the output for an empty string is this …

One for you, one for me.

Of course that code does pass all tests, so it is does concur with the requirements of the exercise.

To raise an exception when extra arguments are passed, we could do this …

def two_fer(*args):
    if len(args) > 1:
        raise TypeError(f"TypeError: two_fer() takes from 0 to 1 positional arguments but {len(args)} were given")
    return f"One for {args[0] if args else 'you'}, one for me."

We could, of course, enable an empty string to be processed as a valid name, as well as dispense with the use of *args, by adding another conditional block to the code.

EDIT (December 11, 2021):

Actually, I may be forced to retract that last claim. Can we enable the argument to be optional, while also being able to distinguish between a default value and an argument passed with that value, without the use of an *args technique? I would be most grateful if someone could demonstrate how that could be done.

Ah, I now realize the intent of that little trick. At least in my reading, the problem statement and tests didn’t appear to make explicit how an empty name should be handled, so my original response above presents options for both. Its a touch longer, but the code I presented for the other case covers this:

While of course a fun exercise and useful for code golf, I’m all in favor of helping beginners make their code much simpler and cleaner (and often spend quite some time on doing so), but don’t think we should be suggesting potentially confusing, unsafe and non-obvious approaches to beginners just to save a few characters. They might take us too seriously :laughing:

Minor nit, but, this repeats the TypeError in the TypeError message, FYI, which will render TypeError: TypeError: .... That nit aside, both the signature and the error message still makes it unclear what this arg should actually be, or even imply what type it is, and it ends up being much more verbose, more complex and error prone than the above, not to mention rather unidiomatic and potentially confusing.

We need not add a conditional block and similar logic, as if "" is a valid name that should not be converted to you, the above two-liner from my original solution should suffice.

As to your edit, I assumed as a precondition that all valid names were strings (or, actually, objects that could be converted to them), and that None was reserved for the default behavior (implicitly, or explicitly triggered by user input). The consumer may want to trigger the default behavior while still passing a value, to avoid different logic between the two cases (e.g. a wrapper for the function), so in this case it is useful to have a singleton value that is not a valid name, but can be passed explicitly by the caller if needed.

However, in the general case, where you have a function that can take arbitrary objects including None, and you need to distinguish between them and your default value (I can recall several major issues in Numpy/Pandas where this was a serious problem, as well as other libraries), then you need a sentinel value. While there are a variety of ad-hoc practices given this is a very common pattern, there is currently an active proposal to add dedicated support for such directly to Python. PEP 665 answers your question in detail, with examples of common problems and common practices to resolve them, along with their limitations, and proposes a better approach in Python itself.

1 Like

Yes, that is absolutely correct. It is all too tempting to become absorbed in an interesting discussion, as this one has been, only to lose sight of the intent of the original post. Accordingly, I enter a plea of guilty. :blush:

Thanks for the link to PEP 665. It is very interesting, and does focus on the issue. … and so I hereby offer you my gratitude.

1 Like