While there are a lot of things that could be fixed or improved in your program, the biggest problem with it is your use of functions and control flow.
One thing that was not at all obvious to me at first glance is that your program runs in a loop, requesting input and giving conversions until the user is done. Consider what another developer needs to read to determine that control flow in your program:
- Top-level module code that calls
mainProg()
mainProg()
which calls makeSelection()
makeSelection()
, where it appears (though not in an obvious way) as if it is supposed to loop (via a recursive call) until a valid selection is entered. (This does not work; more on this later.)
- One of
CtoF_Calc()
or FtoC_Calc()
(well, both really, if you want to make sure of what's going on in both cases) where you read through some moderately complex control flow to see that the exit is eventually via quitContinue()
.
- At this point your reader may have the idea from the name, or he may read through
quitContinue()
to see that it could either exit (unwinding the long stack of functions you've called to get to this point) or call mainProg()
again, which causes the whole program to loop.
That's a pretty complex procedure, involving reading most of the code of the program, to get the overall control flow!
The idea behind dividing a program into functions is to let developer look at things at a higher level of abstraction, that is, to be able ignore smaller details and look at just the main points. To do this, you need to have those main points together, with only the less important details (for that level) pushed away, which this program does not do. So let's look at how we could do that here.
First, you can divide up the code in any Python script or module into two basic parts: the stuff executed "now" as the interpreter reads through the code, and the stuff stored to be executed later. Code at the "top level" outside of functions is executed immediately:
print("Hello")
will immediately print "Hello" to the output. Anything in a function is stored to be executed later:
def printHello():
print("Hello.")
does not immediately print "Hello," but waits until the function is called.
The only code in your program that's immediately executed is the if __name__ == "__main__": ...
paragraph.
For reasons I won't get into here (but related to importing modules), you want as much code as possible to be stored to execute later so I would change that to just:
if __name__ == '__main__':
main()
leaving out the print
statements from that part; move those to be inside your main()
function. Now all you have to do is write functions from this point out.
The main()
function should show the flow of control of the program at the highest (most abstract) level. With appropriate function naming, this can be read alone to give the overall idea of how the program works:
def main():
while True:
selection = read_selection()
if selection == 'q':
return
elif selection == 'c':
convert_C_to_F()
elif selection == 'f':
convert_F_to_C()
else:
print('Invalid selection')
You'll note that here already, though we don't know the details of how any of the conversions work, it's clear that:
1. This program loops until it a decision is made to quit (while True:
).
2. It exits (via return
from main()
) on the user selecting q
.
3. On the user selecting c
or f
it does the conversion and (because there's no return
for either of these) loops to read another selection.
4. An invalid selection prints and error and, again with no return
, loops.
In other words, we have in this one space the full high-level operation of the program. (There are actually some slightly cleaner ways of handling this, but I think that this code best gets the point across to beginners.)
Now all that remains is to write the read_selection()
, convert_C_to_F()
and convert_F_to_C()
functions, which I will leave as an exercise for you. However, one thing I would strongly suggest you do in your initial version of this is to keep all looping control out of these functions. That is, regardless of whether the convert_C_to_F()
gets valid input or not, always have it just take input, print something, and return. If it gets an invalid input temperature you can simply print an error message and return, letting the top level take care of letting the user try again. (She'll have to enter c
or f
again, but that's hardly a huge inconvenience.)
Once you've got that working, you can consider extending those functions to request another temperature if the given one is invalid, but before you do that I'd encourage you to look at the two functions convert_C_to_F()
and convert_F_to_C()
and see if there's common code in the two that you can factor out into their own functions. As a hint, one of the first things you'll probably see is that getting the number, converting it with float()
and handling the potential exception there is common to both and can be extracted to a separate function.
This was long, but I hope that this gives some sense of the need to look at overall program structure. There are lots of small errors, you've made as well, but these both have less effect on overall program readability and are much more easily fixed than problems with the overall program structure.
EDIT: Regarding the comments below about having one function get the number to convert and then call another function to do the conversion, here's an abbreviated code sample to explain what I mean by passing one function to another function for the latter to call. I've trimmed this down quite a bit to express just the core idea and give an example of its use; you can work from this idea to add it to the more sophisticated program (which includes user input to select the type of conversion, etc.) in the original question.
def c2f(t):
' Convert Celsius temperature `t` to Fahrenheit '
return t*1.8 + 32
def f2c(t):
' Convert Fahrenheit temperature `t` to Celsius '
return (t-32)/1.8
def convert(f):
' Read a value and convert it using function `f`. '
n = float(input(' Enter value: '))
print(' Converts to: ', f(n))
def main():
print('Converting C to F:')
# Notice here how we give just the function name, `c2f`,
# without calling it by adding parens () to the end.
# Convert will bind this function to its parameter `f`
# and then can later call it with `f(n)`.
convert(c2f)
print('Converting F to C:')
convert(f2c)
The ability to pass functions to other functions, return them from functions and assign them to variables is referred to as having "first class functions", and is part of a powerful suite of techniques known as functional programming. Languages vary in their support for this; some languages (such as Haskell) are built around these techiques, other provide almost no support at all for them.
Python falls in the middle; it wouldn't be considered a full-fledged functional programming language, but it does provide a fair amount of support for functional programming and some techniques, such as this one, are very commonly used. See, for example, the built-in map()
function that can replace some types of for
loops.
Python decorators, which you probably not heard of yet, are a classic example of something that looks like a special language feature but is in fact just pure functional programming (passing around functions) with only a tiny bit of syntax added. The realpython.com decorator tutorial goes into a lot more detail about first-class functions and the rest of this, and is well worth reading when you feel ready.
value < -273.5 or value > 5.5**30
can be replaced with-273.5 < value < 5.5**30
which to my eyes is a little more readable. \$\endgroup\$