Python palindrome program

I have written a palindrome program. And I was just wondering if it correct? I checked using the words racecar and madam, and it says they are palindromes. I have also tested nonpalindrome words and it worked correctly.

Is my implementation correct? If it's not could you correct it for me. It it works correct, is there an easier way to implement this?

def is_palindrome(x):

for i in range(0,len(x)):
x = x.lower()
if x[i::1] == x[::-1]:
return("%s Is a palindrome" %(x))
else:
return("%s Is not a palindrome" %(x))

print(is_palindrome('racecar'))


I would expect a function that has a "predicate" name (is_x()) to return a boolean value rather than a string. The boolean is much more useful to the caller.

We should add a main-guard to top-level code, so the file could be useful as a module.

Consider adding some unit tests. A convenient way to do this is using the doctest module, which finds tests in the function's docstring; any line beginning with >>> is a test, and is immediately followed by the expected result.

Building on the version shown by C.Nivs, that would give this improved version:

def is_palindrome(x):
'''
True if the letters of x are the same when
reversed, ignoring differences of case.
>>> is_palindrome('')
True
>>> is_palindrome('a')
True
>>> is_palindrome('a0')
False
>>> is_palindrome('ab')
False
>>> is_palindrome('ab a')
False
>>> is_palindrome('Aba')
True
>>> is_palindrome('Àbà')
True
'''
x = x.lower()
# Alternative (letters only):
#     x = ''.join(filter(str.isalpha, x.lower()))
# Alternative (letters and digits only):
#     x = ''.join(filter(str.isalnum x.lower()))
return x[::-1] == x

if __name__ == '__main__':
import sys
for string in sys.argv[1:]:
# check each command-line argument
message = 'is a palindrome' if is_palindrome(string) else 'is not a palindrome'
print(f'{string} {message}')
else:
# no arguments, so run self-test
import doctest
doctest.testmod()


It may well make sense to separate the pre-processing from the palindrome checking, so that is_palindrome() simply returns x[::-1] == x, and the caller is responsible for deciding which characters are significant. We could call it with a specific canonicalisation:

is_palindrome(string.lower())


More advanced would be to pass a normalisation function and/or a character filter, as optional arguments:

def is_palindrome(s, normalise=None, char_filter=None):
'''
True if the letters of x are the same when
reversed, ignoring differences of case.
>>> is_palindrome('')
True
>>> is_palindrome('a')
True
>>> is_palindrome('a0')
False
>>> is_palindrome('a0', char_filter=str.isalpha)
True
>>> is_palindrome('ab')
False
>>> is_palindrome('ab a')
False
>>> is_palindrome('Aba')
False
>>> is_palindrome('Aba', normalise=str.lower)
True
>>> is_palindrome('Àbà', normalise=str.lower)
True
'''
if normalise:
s = normalise(s)
if char_filter:
s = ''.join(filter(char_filter, s))
return s == ''.join(reversed(s))


Usage of these optional arguments should be apparent from the extra tests I've added.

Bug

Whether we use [::-1] or reversed() to reverse the input string, we have a bug when the input contains characters that are formed using combining accents, as demonstrated by these additional tests:

>>> is_palindrome("o̅o") # o+overline, o
False
>>> is_palindrome("o̅o̅") # o+overline, o+overline
True


Both of these fail, because we reverse the string by treating each Unicode character independently, rather than considering them as (possibly multi-character) letters. To fix that, we'll need to find or create a combining-character-aware function to reverse a string - I'm not aware of such a thing in the Python standard library.

• Yeah im a beginner if you can't already tell lol but I mean the program is still working and putting out positive results but I guess I need to make it more clearer and organized Jan 30 '20 at 1:37

You don't need the loop. Once i > 0, the if statement will always be False:

x = 'abba'
x[0::1] == x[::-1]
True

x[1::1] == x[::-1]
False

x[1::1]
'bba'


So drop the iteration. Besides, the immediate return won't allow for iteration anyways, a second iteration will never be reached:

def is_palindrome(x):
x = x.lower()

if x[::-1] == x:
return f'{x} is a palindrome'
else:
return f'{x} is not a palindrome'


You don't need the parentheses with return, and I'd use f-strings instead of % formatting if you're on python 3.5+.

If you are adding the possibilities of spaces/punctuation, you could change x to only include alphabet chars:

def is_palindrome(x):
# this will produce only alphabet lowercase chars
# and join them into a string, though I'm not sure if
# this is a use case for you
x = ''.join(filter(str.isalpha, x.lower()))

if x[::-1] == x:
return f'{x} is a palindrome'
else:
return f'{x} is not a palindrome'


To show what that join statement does:

# maybe this should be a palindrome if you ignore the
# non-alpha chars including spaces
x = "ABC1234CB a."

y = ''.join(filter(str.isalpha, x.lower()))
'abccba'


Edit

To address concerns in the comments, if you wanted to offer options into what kind of filtering you want to provide, you could use a dictionary to act as a mapping:

from functools import partial

def is_palindrome(input_str, filter_type='nofilter'):
"""
Parameter 'filter_type' defaults to pass-through, but you can
provide options such as 'alphanum', 'nospace' (to just get rid of spaces), and 'alpha'
"""
filters = {
'alpha': partial(filter, str.isalpha),
'alphanum': partial(filter, str.isalnum),
'nospace': partial(filter, lambda char: not char.isspace()),
'nofilter': partial(map, lambda char: char) # this is just a pass-through
}

# raise this exception just so the use is more clear to the user
# what is expected
try:
f = filters[filter_type]
except KeyError as e:
raise ValueError(
f"Invalid filter_type, choose one of {'\n'.join(filters)}"
) from e

x = ''.join(filter_type(input_str.lower()))

# you can just check the first half of the string
# against the last half reversed, rather than comparing the entire string
midpoint = len(x) // 2
if len(x) % 2: # even length
a = b = midpoint
else:
a, b = midpoint + 1, midpoint

return x[:a] == x[b::-1]


partial will bind arguments to a function and return a new callable. As a small example:

def f(a):
return a

g = partial(f, 1)
f(2)
2

g()
1


Which is helpful for taking a function that takes many arguments and returning one that takes fewer arguments.

Credit to Toby Speight for returning bool type, @Baldrickk for midpoint slice, and @Peilonrayz for concern over input filtering.

• Why have you added str.isalpha? 0w0 is a palindrome, not because you mutilate it to become w. Jan 30 '20 at 14:00
• @Peilonrayz sure, that didn't appear to be OP's use case. I didn't see any clarification from OP that this needed to be the case, and I don't see how it makes the review wrong. I also mentioned that OP could have this option, but it's not necessary Jan 30 '20 at 14:49
• @Peilonrayz it could easily be made to be alphanumeric, which would make sense, but it does show the method by which would be done, so I wouldn't fault the answer for this. Jan 30 '20 at 14:53
• An additional note - you don't need to compare the full string x with its reverse - you need only compare the first half with the reverse of the second half. Jan 30 '20 at 14:55
• @Baldrickk And why o' why would I ever want my is_palindrome to mangle my input? Not only does it violate SRP, it makes absolutely no sense for it to be there. If you want to check if only the alpha is a palindrome, then make a filter_to_alpha function and use is_palindrome(filter_to_alpha(value)). Jan 30 '20 at 15:55