1

I tried to write a code that can distinguish the following four different errors.

  1. TypeError: The first parameter is not an integer;
  2. TypeError: The second parameter is not a string;
  3. ValueError: The value of the first parameter is not in the range of 1 to 13; or
  4. ValueError: The value of the second parameter is not one of the strings in the set {'s', 'h', 'c', 'd'}.

However, I only can get the first one to work but not the other three errors. I tried different ways to make it work, but still can't figure out what's wrong.

class Card: # One object of class Card represents a playing card

    rank = ['','Ace','Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten','Jack','Queen','King']
    suit = {'d':'Diamonds', 'c':'Clubs', 'h':'Hearts', 's':'Spades'}

    def __init__(self, rank=2, suit=0): # Card constructor, executed every time a new Card object is created
        if type(rank) != int:
            raise TypeError()
        if type(suit) != str:
            raise TypeError()
        if rank != self.rank:
            raise ValueError()
        if suit != 'd' or 'c' or 'h' or 's':
            raise ValueError()
        self.rank = rank
        self.suit = suit

    def getRank(self): # Obtain the rank of the card
        return self.rank

    def getSuit(self): # Obtain the suit of the card
        return Card.suit[self.suit]

    def bjValue(self): # Obtain the Blackjack value of a card
        return min(self.rank, 10)

    def __str__(self): # Generate the name of a card in a string
        return "%s of %s" % (Card.rank[int(self.rank)], Card.suit[self.suit])

if __name__ == "__main__": # Test the class Card above and will be skipped if it is imported into separate file to test
    try:
        c1 = Card(19,13)
    except TypeError:
        print ("The first parameter is not an integer")
    except TypeError:
           print ("The second parameter is not a string")
    except ValueError:
        print ("The value of first parameter is not in the range of 1 to 13")
    except ValueError:
        print ("The value of second parameter is not one of the strings in the set {'s','h','c','d'}")

    print(c1)

I know maybe it is due to that I have same TypeError and ValueError. Therefore, Python can't distinguish the second TypeError which I hit c1 = Card(13,13) is different from the first TypeError. So, I only get the message that "The first parameter is not an integer" when I have c1 = Card(13,13).

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Wu Chu
  • 5
  • 3
  • Thank you, Jonrsharpe. Very Helpful. I followed both your suggestion and the link you posted above, but my code at the end can't give me the right output if I input c1 = Card(12, 'h') since it still gives the ValueError. For my second ValueError in suit, I wrote if suit != self.suit: raise ValueError("The value of second parameter is not one of the set.... And for the try/except at the end of my code, I wrote same try with c1 = Card(12, 'h'). except (TypeError, ValueError) as err: print(err) else: print(c1). But, it still give me the ValueError. What's wrong in my code? – Wu Chu Feb 15 '15 at 20:35

1 Answers1

1

You are trying to distinguish between the error sources in completely the wrong place. By the time the error gets out of Card.__init__, there is no way to tell why e.g. a TypeError was thrown. For each error class (TypeError, ValueError) only the first except will ever be triggered:

try:
    ...
except TypeError:
    # all TypeErrors end up here
except TypeError:
    # this is *never* reached
except ValueError:
    # all ValueErrors end up here
except ValueError:
    # this is *never* reached

Instead, you should provide the specific error messages inside Card.__init__, when you actually raise the error and already know what the reason is:

if not isinstance(rank, int):  # better than comparing to type
    raise TypeError("The first parameter is not an integer")

Then you can handle them much more simply:

try:
    c1 = Card(19,13)
except (TypeError, ValueError) as err:  # assign the error to the name 'err'
    print(err)  # whichever we catch, show the user the message
else:
    print(c1)  # only print the Card if there were no errors

If you have a particular need to distinguish between different errors of the same class, you can either explicitly check the message:

except TypeError as err:
    if err.args[0] == "The first parameter is not an integer":
        # do whatever you need to

or create your own, more specific Exception sub-classes, so you can have separate except blocks:

class FirstParamNotIntError(Exception):
    pass

(this is just an example, they are too specific in your particular case).


It's probably worth having a read through the documentation on exceptions and the tutorial on using them.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • 1
    I will just add that comparing exception messages, in general, should be used only as a last resort, and especially so if the code raising the exception is not yours. Exception messages do get changed, so It creates very brittle code. Certainly don't be afraid of creating your own Exception (sub)class if it allows you to capture better information about the error. – alexh Feb 14 '15 at 20:06
  • Thank you, Jonrsharpe. Very Helpful. I followed both your suggestion and the link you posted above, but my code at the end can't give me the right output if I input c1 = Card(12, 'h') since it still gives the ValueError. For my second ValueError in suit, I wrote if suit != self.suit: raise ValueError("The value of second parameter is not one of the set.... And for the try/except at the end of my code, I wrote same try with c1 = Card(12, 'h'). except (TypeError, ValueError) as err: print(err) else: print(c1). But, it still give me the ValueError. What's wrong in my code? – Wu Chu Feb 15 '15 at 20:35
  • @WuChu why would you expect `suit != self.suit` to work?! How could something that you're *just* checked is a string be equal to a dictionary? Perhaps you should have a look at http://stackoverflow.com/q/15112125/3001761. Also, using the same name for a class attribute and an instance attribute is a bad move. – jonrsharpe Feb 15 '15 at 20:38
  • @jonrsharpe Ok, I can fixed the suit != self.suit into suit != 'd' or 'h' or 'c' or 's' or suit not in self.suit. But, what do you mean for the same name for a class attribute and an instance attribute is bad move? Is this related to why I am not getting the correct output if I input c1 = Card(13, 'd')? – Wu Chu Feb 15 '15 at 21:28
  • That is still not correct - did you **read** the question I just linked? Also, I mean you are using e.g. `rank` for both the list of valid ranks, and the current instance's rank. – jonrsharpe Feb 15 '15 at 21:29
  • First of all, do you mean that I need to have different name for the list of valid ranks and the current instance's rank? For example, should I keep use rank for the list of valid ranks, and switch to rank1 for the current instance's rank? However, I thought my class Card would not work since it is Card(rank, suit). If I change it, how could the code recognize it? If it is still not correct, should I use " suit not in ('d', 'h', 'c', 's')" instead? – Wu Chu Feb 15 '15 at 21:46
  • @WuChu 1. Yes, e.g. `VALID_RANKS` and `rank` (the name of the `__init__` parameter doesn't really matter, `rank` is fine). 2. Yes, or `suit not in 'dhcs'`. – jonrsharpe Feb 15 '15 at 21:47
  • @jonrsharpe I see. Now, I changed my list of valid ranks and list of valid suits into valid_rank and valid_suit without changing the name of the __init__parameter(self, rank, suit). Then, I modified my code associated with it (eg. Card.suit into Card.valid_suit). Then, my code runs normally if I input c1 = Card(1, 's') with Ace of Spades. – Wu Chu Feb 15 '15 at 21:56