Program to check if a date is valid or not

I am a novice programmer and am currently learning Python programming by myself through a book. I stumbled upon a question in a chapter and it took me 2 days to figure out a solution for it. Although, it may not be an efficient code for the problem, I would be grateful if you guys could review it. Just to let everyone know of my Knowledge level on programming, I just have basic knowledge of functions, sequences and decision structure.

Write a program to check if a date is valid or not? The date should be in the mm/dd/yyyy format.

Pseudocode:

• Define 3 functions to check the validity of month,day and year, if valid return True for each function.
• input a string date separated by a "/" and assign it to a date variable.
• split the date variable to 3 separate variables.
• check if the 3 variables are True or not.
• Print if the date is valid or not.

I didn't take into account leap years where February has 29 days and also assumed the year to start at 1 A.D. to current years.

def monthcheck(month):
if month > 0 and month <= 12: ## If month is between 1 and 12, return True.
return True
else:
return False

def daycheck(month,day):
monthlist1 = [1,3,5,7,8,10,12] ## monthlist for months with 31 days.
monthlist2 = [4,6,9,11] ## monthlist for months with 30 days.
monthlist3 = 2 ## month with month with 28 days.

for mon in monthlist1: ## iterate through monthlist1.
if month == mon: ## Check if the parameter month equals to any month with 31 days.
if day >=1 and day <= 31: ## If the parameter day is between 1 and 31, return True.
return True
else:
return False

for mon in monthlist2: ## iterate through the monthlist with 30 days.
if month == mon: ## check if the parameter month equals to any month with 30 days.
if day >= 1 and day <= 30: ## if the parameter day is between 1 and 30,return True.
return True
else:
return False

if month == monthlist3: ## check if parameter month equals month with 28 days.
if day >=1 and day <= 28: ## if the parameter day is between 1 and 28,return True.
return True
else:
return False

def yearcheck(year):
if len(year) >= 1 and len(year) <= 4: ## Check if year has between 1 to 4 numbers and return True.
return True
else:
return False

def main():
date = str(input("Enter the date in mm/dd/yyyy format: ")) ## Input date in the given format.

month,day,year = date.split("/") ## split the date into 3 separate variables.

monthvalidity = monthcheck(int(month))

dayvalidity = daycheck(int(month),int(day))

yearvalidity = yearcheck(year)

if monthvalidity == True and dayvalidity == True and yearvalidity == True: ## check if all 3 variables are valid or True
print("The date {0} is valid.".format(date))

else:
print("The date {0} is invalid.".format(date))

main()

• Welcome to hell. Validating a date is one of the hardest (programming) problems ever. For example you are not checking if the year is n the range of dates when the gregorian calendar is valid. So you may return true for a date which does not exist. Read about the falsehoods programmers believe about time here and here.
– allo
Jul 31, 2018 at 9:28
• For a simple example from the lists: You are not handling leap years at all, so you will reject valid dates. When you start to, note that there are special rules for years divisible by 4 not being leap years, i.e. being divisible by 100. There are more exceptions, which do not fit into a comment. Date calculations are very very hard.
– allo
Jul 31, 2018 at 9:32
• @allo you're right, but 1. in some cases, simple checks like these will suffice (i.e. if you have some degree of control over what types of dates serve as input), and 2. this is an exercise from a book, so it's quite unfair to expect OP to handle every possible edge case when this is only intended as a programming problem to get better at Python. Jul 31, 2018 at 12:25
• @Daniel: I don't think you should or can handle every edge case in your software. This would be ... hell. But at least some leap year handling would be nice to have and with the 4/100/400 rule a nice exercise for a real-life fizzbuzz ;-). The rest of the article is more about having heard of these problems the next time you're implementing something date related and think about if you need to consider them or not.
– allo
Jul 31, 2018 at 13:54
• Btw: ISO 8601 is now 30 years old. Jul 31, 2018 at 13:57

Good work! While there is certainly room for improvement, it is good that you stuck it out and discovered a solution on your own.

1. def monthcheck(month):
if month > 0 and month <= 12: ## If month is between 1 and 12, return True.
return True
else:
return False


Can be more succinctly written as

def monthcheck(month):
return 0 < month <= 12

2. While writing comments for practically every line of code can help you learn, and is a good way to keep track of where you are when working through pseudocode, don't just write what the code already told you.

3. PEP 8 - the style guide for Python - recommends using snake_case for naming functions, so monthcheck would become month_check.

4. What happens if I input "test" when asked for a date? The program crashes. It's a good idea to validate that any input from the user is in the format you expect.

5. The program asks for input in the format mm/dd/yyyy but will accept m/d/y, which probably isn't desired.

6. Don't compare explicitly to True / False

if monthvalidity == True and dayvalidity == True and yearvalidity == True:


Can be more easily read if it is written as

if monthvalidity and dayvalidity and yearvalidity

7. daycheck has several locations in which it could be improved. You could use a dictionary (as heather does), but if you haven't learned about those yet, you can still improve the algorithm.

• Python's in operator can also check if a value is in a list, so instead of looping over monthlist1 and monthlist2, you can just check if month in monthlist1.
• Since monthlist1 doesn't really describe what is in the variable, and I am having difficulty coming up with a better name, I would just inline the list with if month in [1, 3, 5, 7, 8, 10, 12] and add a clarifying comment.
8. Don't be afraid to use natural sounding variable names. I believe monthcheck is better named is_month_valid.

9. What about negative numbers? 1/1/-111 is apparently a valid date. This might be desired, but probably deserves a comment.

10. Converting numbers to strings to determine the size is generally not a good idea, and can get you into trouble in more complex cases with floats / doubles. I would prefer the check 1 <= year < 10000.

11. There is no need to have str() around an input function, input returns a string already.

Python, as a "batteries included" language, has a feature that can do this for us! In production code, this is what you should use.

from datetime import datetime

def main():
user_input = input('Enter the date in mm/dd/yyyy format: ')

try:
datetime.strptime(user_input, '%m/%d/%Y')
print('The date {} is valid.'.format(user_input))
except ValueError:
print('The date {} is invalid'.format(user_input))

if __name__ == '__main__':
main()


This was rather long, but don't worry! It is completely normal to get a lot of feedback when you are starting out. The more you code, the more natural it becomes.

• A few suggestions: you can make your code a little less verbose by using from datetime import datetime and if you have Python 3.6+, can use print(f"The date {date:%m/%d/%Y} is valid."). Finally, just a small note - date is a reserved word in datetime, I often use 'today' or 'date_val' instead of the word 'date'. Of course, if you don't do a from datetime import datetime, date then it shouldn't matter, but it helps to avoid naming clashes so you're working on the expected variable and not the library function accidently. Jul 31, 2018 at 4:49
• Thanks! I don't with Python much so it looks like I have a lot to learn too. Jul 31, 2018 at 11:58
• Minor nit. Instead of is_month_valid, I think I'd use month_is_valid as it reads better: if month_is_valid and .... Jul 31, 2018 at 14:11
• Thank you @Gerrit0 for your valuable feedback. it really motivated me to learn more and keep going. I haven't really learned about dictionaries and i haven't really soaked up exception handling. The next time I post any code, I will try to make it better. Thank you again for setting aside your valuable time to review my code. Jul 31, 2018 at 18:10
• @rcollyer I'm not a Python programmer, but it is very normal to name boolean variables with a yes/no verb prepended: if is_valid_month, if is_leap_year, if has_time, etc. You read it in your head as "if it is a valid month ..." Aug 1, 2018 at 13:57

For the most part, looks pretty good to me!

Format

You didn't use underscores to separate the words in a lot of variable names, which kinda bothered me. Snake_case is recommended by PEP8, so I changed that.

You also added extra indentation in your first function (whether that was just the copy-paste of your code into the question messed up or not, I dunno) so I removed it.

Algorithm

Your day_check was the thing I changed the most. I basically just created a dictionary matching the month numbers with the day numbers and checked if the day inputted in the function was between 0 and the max defined by the dictionary. I felt that significantly shortened and clarified the function.

The other change I made was to change your if statement in main to if month_validity and day_validity and year_validity: which I personally felt was cleaner.

If you're using main()...

Might as well make sure you're in the right spot when calling main(), so I added the last if statement.

Poor February

Leap years aren't too bad to check for. Simply add the year as a parameter to month_check and do

if int(year) % 4 == 0:
if int(year) % 100 == 0:
days_in_month[2] = 28
else:
days_in_month[2] = 29
else:
days_in_month[2] = 28


User input check

As Gerrit0 pointed out

What happens if I input "test" when asked for a date? The program crashes. It's a good idea to validate that any input from the user is in the format you expect.

This can be fixed with a simple try/except - try all the code in the main() function, except ValueError and print a reminder to the user of the proper format.

Negative Dates

Gerrit0 also points out that the function allows for negative numbers (reminds me of this XKCD) in the year. While I didn't add this to the code, I'd change the year function to making sure the actual number is in the proper range, which then allows for the nicety that you can do all your type conversions on the inputs directly instead of before each function input. It also provides a nice way to make sure the date is in the Gregorian calendar.

Final Code

def month_check(month):
if month > 0 and month <= 12: ## If month is between 1 and 12, return True.
return True
else:
return False

def day_check(month, day):
days_in_month = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:31}
if 0 < day <= days_in_month[month]:
return True
else:
return False

def year_check(year):
if len(year) >= 1 and len(year) <= 4: ## Check if year has between 1 to 4 numbers and return True.
return True
else:
return False

def main():
date = str(input("Enter the date in mm/dd/yyyy format: ")) ## Input date in the given format.

try:
month,day,year = date.split("/") ## split the date into 3 separate variables.

month_validity = month_check(int(month))

day_validity = day_check(int(month),int(day))

year_validity = year_check(year)

if month_validity and day_validity and year_validity: ## check if all 3 variables are valid or True
print("The date {0} is valid.".format(date))

else:
print("The date {0} is invalid.".format(date))
except ValueError:
print('Your input was not valid. Please enter the date in a mm/dd/yyyy format.')

if __name__ == "__main__":
main()

• Thank you @heather for reviewing my code. I ve started implementing the proper way to name functions and variables. I didn't understand the part of using main(). As for the input check, it falls under exception handling? I'll read more on that part. Jul 31, 2018 at 18:15
• @mathlover92 So what the if __name__ == "__main__": does is check if you are running that file from that file's location - i.e., whether or not you are importing it. This is useful because if you are importing the file, it runs any "loose" code. So if you wanted to import your main() function into another file by doing from filename import main then it would automatically run main and you probably don't want that. The if statement prevents that from happening. And yeah, the input check is exception handling and it's very useful to control program flow [...] Jul 31, 2018 at 18:22
• @mathlover92 just keep in mind when using exception handling that it's not very computationally expensive unless you catch an exception. So if you're only catching exceptions now and again, like here, with user error, then it's a good spot to use it. It basically just runs the code, and if it runs into the error ValueError it goes into the other part of the code that explains what to do if that happens. If an error other than ValueError is raised, then you see it, which is why you generally don't want to do try: ... except: ...  because it means that it catches all errors, [...] Jul 31, 2018 at 18:24
• @mathlover92 [...] whether or not they're the right one, which means if your program isn't behaving as expected, you can't see any of the other errors (or in our case ValueErrors unrelated to user input) popping up. That's one thing to be careful about using them. Tl;dr, look up exceptions =) Jul 31, 2018 at 18:25
• thank you @heather i'll find some problems to practice exception handling so i can get a good grip on it..and is exception handling used in almost all programs? do u recommend any good site for beginners to practice python programming problems? Jul 31, 2018 at 20:35

Following on from heather's answer with simplified functions.

Simplified month check return:

def month_check(month):
return 0 < month <= 12


This is a different implementation of day_check, using a plain list rather than a dictionary, and a simplified return:

def day_check(month, day):
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
return 0 < day <= days_in_month[month-1]


Simplified year_check return

def year_check(year):
return 1 <= len(year) <= 4


This doesn't answer the OP's question, but wanted to mention dateutil as a shortcut for parsing many different date string styles into a date (or datetime) object.

from dateutil.parser import parse

def valid_date(date_value):
try:
return parse(date_value).date()
except ValueError as e:
logger.exception(f"Not a valid date: '{date}' - reason: {e}.")


There are really great answers here, but i want to show you how do i think when i see comments (and every time i want to add one).

Comments are used normally when you want to clarify your code. But, if your code is clear enough, the comment will become redundant. And reading redundant things makes ourselves feels like we are wasting time, so naturally the comment start to be innecesary: deletable.

I will suppose you understood the given answers by others here, so i will focus on comments:

def is_month_valid(month):
return 0 < month <= 12 ## If month is between 1 and 12, return


If you are in a situation where you want to keep your comment because you find it clearer than your code: Make your code look a bit more like that comment.

For example, you could try thinking:

return 0 < month <= 12    ## If month is between 1 and 12, return
return 1 <= month <= 12
return is_between(month, 1, 12)
return is_the(month).between(1, 12)
return comparable(month).is_between(1, 12)  ## Or you could call is_month_valid(comparable(month)) instead and this gives you...
return month.is_between(1, 12) ## If month is between 1 and 12, return
## Also, if month.is_between(1, 12): return... I think the other answers actually answers why this should be discarded. If not, keep reading and think why.


All of them are valid solutions. And if you see, the last one will make the comment really redundant:

def is_month_valid(month):
return month.is_between(1, 12) ## If month is between 1 and 12, return


So, why you will keep this comment? The code is clear, and actually, that comment is making your code look unclean, so you don't have another choice than delete that.

Yay!!... Fixed? ... Is this better? We just focus on readability and nothing else? Not exactly. You have to keep in mind the complexity that each solution has. So:

return 0 < month <= 12    ## If month is between 1 and 12, return


This is a pretty good solution. Why? Is readable to the point it makes the comment redundant. And you don't need nothing else to make it work. But

return 1 <= month <= 12    ## If month is between 1 and 12, return


Is this better? The comment now is more redundant. And you didn't add complexity. This is like a free change. Also, this doesn't allow numbers between (0, 1) as valid months. So yes, this is actually better.

return is_between(month, 1, 12) ## If month is between 1 and 12, return


Well, at least for me, this is less legible than previous ones. And needs you to create another function to make it work. Is this function worth enough? Well, one could point that if you create it you could reuse that function in different places, like in is_between(year, 1, 10000). And it allows you to centralize integer validations here... But, you have to decide. That's programming actually: Take decisions and justify them. (Like naming things...)

return is_the(month).between(1, 12) ## If month is between 1 and 12, return


This is actually more redundant and more readable. But, now, you've created a class (another function) with an arguable name, and also another function associated (like a method). However, this maintains the pros from last solution. At this point, this is starting to feel like an unnecessary over engineered solution, but we are just explaining our thinkings.

return comparable(month).is_between(1, 12) ## If month is between 1 and 12, return


Why is_the an arguably name? Think that some time has passed and you see a function called is_the. My main reaction would be "What the heck is this thing!". It says nothing. You can't know it's purpose without reading it inner code. So, this solution fixes a bit that arguably name. Now month is comparable to 1 and 12. However, this is still over engineering for a little advantage (helps to dry).

return month.is_between(1, 12) ## If month is between 1 and 12, return


Now, this is better. That code is more redundant and readable than previous solutions. But, what is its price? Now, every time you want to call is_month_valid you depend on sending a comparable object first: is_month_valid(comparable(month)). And don't forget you still have to create comparable and is_between too to make this work. But, is this over engineered solution needed? That will depend on how necessary is this in your program: how much this helps to dry, and how probably is this going to change in the future. If it's not stable, could cause more problems than fixes.

Of course, for every solution exists a better one at a particular time. To get them you just have to continue over engineering it and balance their pros and cons. I will not continue doing that because i think is clear now. And looking at the size of your program, i'm sure the last solution is too much, while the second one (1 <= month <= 12) is simple and clear.

And keep doing the same for every comment you have, or you are tempted to make.

monthlist1 = [1,3,5,7,8,10,12] ## monthlist for months with 31 days.
monthlist2 = [4,6,9,11] ## monthlist for months with 30 days.
monthlist3 = 2 ## month with month with 28 days.


Wouldn't those lines be replaceable with this?

months_with_31_days = [1,3,5,7,8,10,12] ## monthlist for months with 31 days.
months_with_30_days = [4,6,9,11] ## monthlist for months with 30 days.
month_with_28_days = 2 ## month with month with 28 days.


months_with_31_days = [1,3,5,7,8,10,12]
months_with_30_days = [4,6,9,11]
february = 2 ## And month_with_28_days? Isn't that just february?.


Also, like @heather said, i would prefer to make a function days_in_month instead of this. But, my focus here is to show how do i think when facing comments.

for month in monthlist: ## iterate through monthlist.


I know this is going to dissapear, but, is that comment actually needed? Is for (each) month in monthlist: less clear than iterate through monthlist? I'm sure most programmers are going to recognize what is this line doing by just reading it. In this case, i think the comment isn't needed.

if month == mon: ## Check if the parameter month equals to any month with 31 days.


This is an interesting comment, because you need some context to understand this code. You can't just read that and know what is happening. You need this picture:

def is_day_valid(month, day):
...
for mon in months_with_31_days:
if month == mon: ## Check if the parameter month equals to any month with 31 days.


Now, you can understand why you use mon instead of month, and why is there another month variable. This situation; when you need some context to understand a line; is undesirable. Luckily, you've put a comment with the required context to understand it, but, is necessary? This was solved by @Gerrit0:

if month in months_with_31_days: ## Check if the parameter month equals to any month with 31 days.


That comment does add noise! Remove it and its perfect. But what can you do if you needed that for?

for month_with_31_days in months_with_31_days:
if month == month_with_31_days: ## Check if the parameter month equals to any month with 31 days.


Now is possible to forget that for line while reading the if line if you want. You don't care which month with 31 days it is, but you know if you enter inside that if block is because month have 31 days. And you can over engineer solutions like we did previously for getting more readability in that single line... Check for example if days_in(month) == 31:, that will move the previous for line from is_day_valid. Try to make functions short and readable. With less lines of code in each function, you have less probability to need more context.

date = input("Enter the date in mm/dd/yyyy format: ") ## Input date in the given format.


If you read that line, you know you are asking the user for an input and saving it in a variable called date. You are actually saying less in that comment. However, redundant and needless. And what about dividing that user interaction into another function. For example:

date = ask_user_for_input_a_date()


Too verbose, and unnecessary in this case. However, i want to point that, when you start to have multiple inputs, you will start asking yourself where to ask those inputs. The same happens with the outputs. One good practice for novice is to divide your main function like:

def main():
inputs = take_inputs() ## You could validate inputs inside if you want
solution = solve_problem(inputs)
show(solution)


You gather all needed information before starting your problem (group your inputs), then solve the problem completely (group your outputs) and lastly show your solution. If you have problems related to inputs, you know where to start looking for. The same applies to the core logic of your problem and final presentation. This is a nice starting point, and fit well on problems that don't need user interaction while solving it.

However, nice start, seriously. I've started my programming life with the same problem; date validation, and my code was a lot worse than yours :)

What you did well

You packaged the code into a main function and three helper functions, each with a specific purpose.

Style

• The most important function to define for this task, is_valid_mmddyyyy(date), isn't defined! That would make your work reusable.

• The comments are excessive, and mainly serve to guide Python beginners. Instead of those comments, it would be better to document the purposes of the functions instead, as docstrings.

• You should rarely need to write True or False explicitly in your code. The pattern

if day >= 1 and day <= 31:
return True
else:
return False


… would be better written as return 1 <= day <= 31.

Similarly,

if monthvalidity == True and dayvalidity == True and yearvalidity == True:


… would be better written as

if monthvalidity and dayvalidity and yearvalidity:

• The names monthcheck, daycheck, and yearcheck don't quite convey how they behave for valid or invalid inputs. Do they print error messages? Do they raise exceptions? It would be better to name them is_valid_month, is_valid_day, and is_valid_year to make it clear that they are predicates (functions that return either True or False with no side-effects).

• It's annoying that monthcheck(month) and daycheck(month, day) accept integers as inputs, but yearcheck(year) accepts a string.

• monthlist1 and its friends are not optimally named, thus necessitating comments like ## monthlist for months with 31 days. Also, monthlist3 is, deceptively, not a list. A slightly better idea:

months_with_31_days = [1, 3, 5, 7, 8, 10, 12]
months_with_30_days = [4, 6, 9, 11]
months_with_28_days = [2]

if month in months_with_31_days:
return 1 <= day <= 31
if month in months_with_30_days:
return 1 <= day <= 30
if month in months_with_28_days:
return 1 <= day <= 28


Validation quality

• You assume that there will be two / characters in the input. If there are fewer parts or more parts, then it crashes with a ValueError.

• If the month or day are not integers, then it crashes with a ValueError.

• You only care about the length of the year, so 1/1/zero and 1/1/3.14 are both considered valid dates.

• As you say, you haven't bothered to support leap years. But the design of the daycheck function makes it hard to enhance the code to add leap year support.

Suggested solution

The main challenge is to determine how many days there are in a month. Your daycheck kind of does that, but falls short in two respects:

• It assumes that the month is valid. For daycheck to do its job, it needs to perform a lookup anyway, so monthcheck doesn't really need to be a separate function. You just need to indicate that the month is not in the lookup table.

• If you wanted to support leap years, the month alone is insufficient: you would also need to know the year.

Keeping that in mind, I would redesign the program this way:

def days_in_month(year, month):
"""
Given a year and month as integers, return the last day of that month
(assuming the Gregorian calendar).  Raise ValueError if the month is
invalid.
"""
if month == 2:
is_leap_yr = (year % 4 == 0) and (year % 100 != 0 or year % 400 == 0)
return 29 if is_leap_yr else 28
try:
return {
1: 31,        3: 31,  4: 30,  5: 31,  6: 30,
7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31
}[month]
except KeyError:
raise ValueError('Invalid month')

def is_valid_mmddyyyy(date):
"""
Test whether the date is a string in mm/dd/yyyy format representing a valid
date (assuming the Gregorian calendar).
"""
try:
mm, dd, yyyy = [int(part) for part in date.split("/")]
return 0 < yyyy <= 9999 and 0 < dd <= days_in_month(yyyy, mm)
except ValueError:
# Too few slashes, too many slashes, non-integer part, or invalid month
return False

def main():
date = input("Enter the date in mm/dd/yyyy format: ")
valid = is_valid_mmddyyyy(date)
print("The date {0} is {1}.".format(date, "valid" if valid else "invalid"))

if __name__ == '__main__':
main()


Almost everything is already said by the previous posted answers. But two notes:

1. if you really use names like monthlist1, monthlist2, monthlist3 then use them in a consistent way: if monthlist1 and monthlist2 are lists of numbers then monthlist3 should be a list of numbers, too. Such a consistent naming makes it easier to read the code. So

monthlist3=[2]

.

# calculate the days of february
if year%4==0:
if year%100==0:
if year%400==0:
days = 29
else:
days = 28
else:
days = 29
else:
days = 28


Most things here are already covered. While the year and the month check are fairly straight-forward, the day check is hard.

def daycheck(year, month, day):
"""
Check if the day is valid.

Parameters
----------
year : int
month : int
day : int

Returns
-------
is_valid : bool
"""
assert  1 <= month <= 12
max_days_per_month = {1: 31, 2: 30, 3:31,
4: 30, 5: 31, 6:30, 7: 31,
8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
if (day > max_days_per_month[day]) or (day <= 0):
return False
if month != 2:
# Not quite true, e.g. September 1752 in the UK had less than 20 days.
return True
dst_applies = (year % 4 == 0) and (year % 100 != 0 or year % 400 == 0)
if not_dst_applies and day >= 29:
return False
return True


Things to learn:

1. Use dictionaries. They make things shorter / easier to understand
2. Yes, February once had 30 days: https://en.wikipedia.org/wiki/February_30#Swedish_calendar
3. Yes, if a local date is valid depends on the city / country, because of calendar system switches.
4. It is capture error cases / easy cases in the beginning. The less deep your code is nested, the easier it is to read / check.