7
\$\begingroup\$

I'm just learning Python and am looking for some feedback on some of the code exercises I've completed and do work. In particular, what would make them more Pythonic, or more closely align with standard practices or what-not.

Here is code I wrote for a loop exercise that works. It gets a license plate number from user, and checks that its formatted correctly. As I said, looking for feedback, but for right now, not looking to import anything, just better ways of writing what I've written. Btw, it's for, I suppose, a popular course, but it's already been turned in, so not trying to cheat, just learn, so any feedback will be appreciated. Thought maybe I should add details of what the code needed to check for to determine if plates were valid or not.

All vanity plates must start with at least two letters.

… vanity plates may contain a maximum of 6 characters (letters or numbers) and a minimum of 2 characters.

Numbers cannot be used in the middle of a plate; they must come at the end. For example, AAA222 would be an acceptable … vanity plate; AAA22A would not be acceptable. The first number used cannot be a ‘0’.

No periods, spaces, or punctuation marks are allowed.

def main():
    
    # Get user input
    plate = get_input()
    
    if is_valid(plate):
       print("Valid")
    else:
        print("Invalid")

def get_input():
    text = input("Enter plate text:")
    return text

def is_valid(s):
    #Check that plate is 2-6 character and starts with 2 letters.
    if len(s) >1 and len(s) <7 and s[0].isalpha() and s[1].isalpha():
        pass
    else:
        return False
        
#check that all text is alpha numeric
    for c in s:
        if c.isalnum() == True:
            continue
        else:
            return False
            
#Check that first number is not 0 and that there are no letters after a number

    i =0
    while i < len(s)-1:
        if s[i].isdigit() and s[i +1].isalpha() or s[i].isalpha() and s[i+1] == "0":
            return False
        i+=1
        
    return True
main()
\$\endgroup\$
10
  • 1
    \$\begingroup\$ You might want to also check that those alpha characters are UPPERCASE alpha... \$\endgroup\$
    – Fe2O3
    Commented Jun 16 at 12:43
  • 1
    \$\begingroup\$ ...unless you live in a country that allows both upper and lower characters in a vehicle registration. But there's probably none of those, as it would make the system almost unusable. \$\endgroup\$ Commented Jun 16 at 12:53
  • 4
    \$\begingroup\$ Have you thought about using a regular expression perhaps? Maybe it's too early but something to consider in the future, which could make the code more compact. \$\endgroup\$
    – Kate
    Commented Jun 16 at 15:21
  • 2
    \$\begingroup\$ Suggest you give some feedback to the course provider. They need to provide a much more specific "spec". For instance, "uppercase letters and digits". 'z' is a 'letter', and '-3' is a 'number'. Require of them that they are professionals who, by example, demonstrate unambiguous intent/meaning. 10% of programming is writing code. 90% of programming is "revising/reshaping" and/or plugging the holes after-the-fact because two minds did not envision the same outcome... \$\endgroup\$
    – Fe2O3
    Commented Jun 17 at 1:05
  • 1
    \$\begingroup\$ Hey, your code looks good and it looks like you even used the One True Formatter Lord named Black for indentation. But my advice that people here would hate is to do what works for you and if that means single character name variables, one class for every 10 redundant functions, or AI generated doc-strings.. you get on with your bad self and to hell with what a fellow Pythonista neckbeard thinks. Unless it's for work.. then you better get in line and smile with every PR you eat. \$\endgroup\$
    – nannerpuss
    Commented Jun 17 at 4:51

5 Answers 5

10
\$\begingroup\$

Overview

The code layout is good, and you used meaningful names for some functions and variables.

Documentation

You should add documentation at the top of the code as a docstring to describe its purpose:

"""

Check if license plate number is formatted correctly.

The user enters a licenses plate string, and the code reports 
if it is valid or not.

"""

Indentation and spacing

There is some inconsistent indentation and spacing.

For example, the comments should be aligned with the code:

# check that all text is alpha numeric
for c in s:

The black program can be used to automatically indent and space things out for you.

Comments

Many of the comments in the code are good, but this one is not needed because you chose a meaningful name for the function:

# Get user input
plate = get_input()

You can delete that comment.

Naming

s is not a good name for a variable. Naming it text would be a little more meaningful.

Prompt

The input prompt message would be more helpful if it included details to guide the user on what constitutes a valid license plate. It would also be helpful to show examples of valid and invalid plates. For example:

def get_input():
    print("A valid license plate consists of:")
    print("  - 2 to 6 characters")
    text = input("Enter plate text:")

Invalid feedback

It would be extremely helpful to the user to see exactly why an entered plate is invalid. This would make the code longer and perhaps the valid check less efficient, but the user will be less frustrated trying to guess why it is invalid:

def is_valid(s):
    # Check that plate is 2-6 characters
    length = len(s)
    if length < 2 or length > 6:
        print(f"Must be 2-6 characters, not {length}")
        return False

    # starts with 2 letters.
    if not s[:2].isalpha():
        print("First 2 characters must be letters")
        return False
\$\endgroup\$
3
  • 19
    \$\begingroup\$ Do you really want to be printing output from a predicate function like that? An alternative is to return the reason, or None if the string is valid; then the calling code can present to the user in the most appropriate manner (e.g. via a message box if used from a GUI application). \$\endgroup\$ Commented Jun 16 at 13:00
  • 4
    \$\begingroup\$ Rather than printing or returning a reason, it's better to have two functions that return booleans. Let the caller handle all display work (string reasons, printing). Alternately, one could use an enum for reasons but this seems a bit overengineered. \$\endgroup\$
    – ggorlen
    Commented Jun 16 at 18:17
  • 3
    \$\begingroup\$ One slight nitpick: I wouldn't say "s is not a good name for a variable", period, because for something like a loop iteration variable in a very short loop, it could be quite reasonable to use a very short variable name. \$\endgroup\$
    – David Z
    Commented Jun 17 at 6:53
11
\$\begingroup\$

pass is very rarely needed in Python.

To take a pattern from the code here:

if condition:
    pass
else:
    return False

We can express that more clearly as

if not condition:
    return False

Similarly, a loop of the form

for:
    if condition:
        continue
    else:
        return False

can be

for:
    if not condition:
        return False

In concrete terms those would be

def is_valid(s):
    # Check that plate is 2-6 character and starts with 2 letters.
    if len(s) < 2 or len(s) > 6 or not s[0].isalpha() or not s[1].isalpha():
        return False

    # check that all subsequent text is alphanumeric
    for c in s[2:]:
        if not c.isalnum():
            return False

Actually, that loop might be better in functional style:

if not all(map(str.isalnum, s[2:])):
   return False

Note that hard-coding the rules for your region makes for code that's inflexible (e.g. it's not applicable where I live: your function would exclude many valid vehicle registration marks, but permit quite a few invalid ones too). It might be better to use a regular expression to test the validity, so that it's easier to use a different rule for each country.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks. Definitely some helpful feedback that I'll start implementing. \$\endgroup\$
    – Th1s0neGuy
    Commented Jun 16 at 13:45
  • 1
    \$\begingroup\$ if len(s) < 2 or len(s) > 6 or ... - since you already touch this line, why not if not 2 <= len(s) <= 6 or ...? \$\endgroup\$
    – STerliakov
    Commented Jun 16 at 22:03
  • \$\begingroup\$ @SUTerliakov, it's easier to comprehend comparisons without not, in my opinion, but I wouldn't object to your suggestion if I were reviewing that. \$\endgroup\$ Commented Jun 17 at 6:32
  • \$\begingroup\$ @TobySpeight Just read this Q&A and want to thank you for the special attention you've given to both my questions and answers, in particular this one, pointing out that addressing 'locale' would improve that answer for a learner's question. Herein, there is a "companion answer" that suggests this Python learner OP turns away from learning Python and use eclectic knowledge of regex syntax instead. It's marvelous that some critiques say "C is not C++", while others say "Use regex in a Python wrapper." Changeable winds, it appears... Cheers! \$\endgroup\$
    – Fe2O3
    Commented Jun 18 at 5:53
11
\$\begingroup\$

Write unit tests. Currently your program is only interactive, but unattended tests will offer better confidence in your logic.

A regex is a pretty clear win here, and will simplify most of your logic, eliminating all of the loops and conditions that you wrote.

import re

LICENSE_PATTERN = re.compile(
r'''(?x)  # Verbose
    (?!   # Negative lookahead
        .{7,}  # Must be less than 7 characters
    )
    [A-Z]{2,}   # Start with letters; minimum of two
    (?:         # Non-capturing group, numbers (must come at the end)
        [1-9]   # First number cannot be '0'
        [0-9]*  # Any other numbers
    )?    # Numbers are optional
    $     # Anchor end
''')


def is_valid(s: str) -> bool:
    return LICENSE_PATTERN.match(s) is not None


def test() -> None:
    assert is_valid('AAA222')
    assert is_valid('XY')
    assert is_valid('ABCDEF')
    assert is_valid('AB1')
    assert is_valid('AB2000')

    assert not is_valid('AAA22A')   # letter at end
    assert not is_valid('AAAAA0')   # first number is 0
    assert not is_valid('ABCDEFG')  # too long
    assert not is_valid('123')      # no letters
    assert not is_valid('A1')       # not enough letters
    assert not is_valid('A')        # too short
    assert not is_valid('A98765')   # not enough letters


if __name__ == '__main__':
    test()

An alternative regex form that leverages the built-in digit class is

LICENSE_PATTERN = re.compile(
r'''(?x)  # Verbose
    (?=   # Positive lookahead
        [A-Z\d]{,6}  # Up to 6 numbers or letters
        $            # ...then the end
    )
    \D{2,}  # At least two non-digits
    (?!0)   # First digit cannot be '0'
    \d*     # Any digits
    $       # Anchor end
''')

The digit class is more inclusive than just the ASCII digits 0-9. Since the specification is ambiguous, this is fine in theory. In reality the specification would need to be more specific.

\$\endgroup\$
4
  • 3
    \$\begingroup\$ While I agree with a regex being a better way to write production code, it takes away a lot of the learning part - writing loops, paying attention to start/end indexes, getting your indexes right, returning false within the loop vs. returning true outside etc. So I don't think "replace your logic by something that lets you omit all the tediousness" is good advice for a learner. \$\endgroup\$ Commented Jun 17 at 11:14
  • \$\begingroup\$ Note that \d matches more than just ASCII digits (e.g. ٠ U+0660 Arabic-Indic Digit Zero). Don't forget the re.ASCII flag: flags = re.A | re.X / (?ax). \$\endgroup\$
    – InSync
    Commented Jun 17 at 18:41
  • 2
    \$\begingroup\$ @GuntramBlohm that's been adequately covered in the other answers. Learners are not stupid: they should be shown how this works in the professional world in addition to how their own code should be improved on a granular level. \$\endgroup\$
    – Reinderien
    Commented Jun 17 at 21:55
  • \$\begingroup\$ Great idea to include the def test() here so that the code tests itself when run manually but not when imported (I presume that's what the __name__ var allows you to check) \$\endgroup\$
    – GuruBob
    Commented Jun 17 at 23:52
8
\$\begingroup\$

When possible and reasonable, make code that looks like the requirement

Given:
#Check that plate is 2-6 character and starts with 2 letters.

Instead of: if len(s) >1 and len(s) <7 and ...

Use: if len(s) >= 2 and len(s) <= 6 and ...

\$\endgroup\$
6
\$\begingroup\$

The whole while loop section at the bottom is unecessary (and a little clunky); you can check for both 'isalphanumeric' and letter following a number by iterating over the string just once by, for instance, keeping track of whether we've seen any numbers yet.

seen_digit = False
for character in s:
    
    if not character.isalnum():
        return False
    
    if seen_digit and character.isalpha():
        return False
    
    seen_digit = character.isdigit()
            
return True
\$\endgroup\$
1
  • \$\begingroup\$ Thanks. I kind of wanted to use a for loop there, but wasn't sure how to make it work so it would check c and the char following it \$\endgroup\$
    – Th1s0neGuy
    Commented Jun 17 at 20:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.