0

I get the error below when I run my code. It says unbound local error:

Could someone explain what I am missing please?

class Car:
    """A simple attempt to represent a car. """

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0
        

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name =f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it. ")

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")
    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles

#moving battery attributes from electric cars to  a separate class for
class Battery:
        def __init__(self, battery_size):
             """Print a statement describing the battery size."""
             self.battery_size = battery_size
             self.battery_size = [100, 75]
        def describe_battery(self):
            """Print a statement describing the battery size."""
            print(f"This car has a {self.battery_size}-kwh battery.")

        def get_range(self):
            """Print a statement about the range this battery provides."""
            if self.battery_size == 75:
                range = 260
            elif self.battery_size == 100:
                range = 315
            print(f"This car can go about {range} miles on a full charge.")

#Electric car subclass

class ElectricCar(Car):
        """Represent aspects of a car, specific to electric vehicles."""

        def __init__(self, make, model, year):
            """Initialize attributes of the parent class.
            Then initialize the attributes of the parent class"""
            super().__init__(make, model, year)
            self.battery = Battery([])


        def fill_gas_tank(self):
            """Electric cars don't have gas tanks."""
            print("This car does not need a gas tank!")


 my_tesla = ElectricCar('tesla', 'model s', 2019)
    print(my_tesla.get_descriptive_name())
    my_tesla.battery.describe_battery()
    my_tesla.battery.get_range()

This car has a []-kwh battery. Traceback (most recent call last): File "electric_car02.py", line 82, in my_tesla.battery.get_range() File "electric_car02.py", line 48, in get_range print(f"This car can go about {range} miles on a full charge.") UnboundLocalError: local variable 'range' referenced before assignment

Please, I am still very new to Python, so would really appreciate your dumbing it down for me.. Thanks.

champ885
  • 11
  • 2
  • First: overwriting the builtin [`range`](https://docs.python.org/3/library/functions.html#func-range) is not the best of ideas. Choose an other name. Second: Your value `range` will only be set if `self.battery_size` is equal to `75` or equal to `100`. For every other value `range` is undefined. – Matthias Nov 22 '20 at 23:22
  • What's the point of defining `fill_gas_tank` if it's not overriding an existing method? – chepner Nov 22 '20 at 23:31
  • Have you considered what should happen when `self.battery_size` is some other value, besides the one you checked for? Have you considered what *will* happen? Now, look at how you set the initial value of `self.battery_size`. Is it either of those possibilities? – Karl Knechtel Nov 23 '20 at 00:01

2 Answers2

2

There are cases that are not covered here for your range variable - you have to provide a default value. Your if ... elif block only covers two cases, so the variable can't be initialized in those remaining cases unless you provide the default value.

Replace:

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315
        print(f"This car can go about {range} miles on a full charge.")

with

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315
        else: # that's what matters here - the default is 90 for now
            raise NotImplementedError("This battery size has yet to be provided, not implemented.")
           
        print(f"This car can go about {range} miles on a full charge.")

Notice that the default is set to 75 and you have to set whatever value you consider appropriate instead. As you have not actually implemented it yet, consider providing some logic or NotImplementedError will be used.

In case you don't want range to be visible outside of those two cases, you have to make sure you print it only for those cases self.battery_size == 75 and self.battery_size == 100 separately, but within corresponding blocks. Then you won't need a separate else:.

Also, consider using keyword argument with default value for initializing your Battery object:

class Battery:
    def __init__(self, battery_size=75):
         """Print a statement describing the battery size."""
         self.battery_size = battery_size

then in your ElectricCar you may do:

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """Initialize attributes of the parent class.
        Then initialize the attributes of the parent class"""
        super().__init__(make, model, year)
        # self.batery = Battery(battery_size=100) for example
        self.battery = Battery() # Default value 75 will be set
dmitryro
  • 3,405
  • 2
  • 20
  • 27
  • @Tomerikoo I did - read the comment right after 0 – dmitryro Nov 22 '20 at 23:53
  • Thanks so much, that worked.. I just need to figure out a way to get this output to show only one item ==> This car has a [100, 75]-kwh battery. So my question would be, can I have a list as a default parameter when defining a class? – champ885 Nov 24 '20 at 00:33
  • 1
    @champ885 you usually don't want to do that: https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument – Tomerikoo Nov 24 '20 at 08:16
1

Here the __init__ method of class battery is a bit odd. You first create an instance attribute self.battery_size that depends on what value was provided when creating an instance but then you immediately overwrite it with this:

self.battery_size = [100, 75]

Now it doesn't matter what value you provide when creating an instance, the value of self.battery_size will always be a list [100, 75].

Further down in the code you have the method to get the range based on the battery size, but the battery size is a list, not an integer so your if/elif will always evaluate to False.

Instead if you specifically want one of the two values just check in the first if statement and raise an error if the value is neither 75 or 100:

        def get_range(self):
            """Print a statement about the range this battery provides."""
            if self.battery_size not in [75, 100]:
                raise NotImplementedError('We do not produce this size of batteries yet')        

            elif self.battery_size == 75:  
                range = 260
            else: 
                range = 315
            print(f"This car can go about {range} miles on a full charge.")

Now because of this the variable range is never created and you get the UnboundLocalError when trying to reference it in the print function.

You need to remove self.battery_size = [100, 75] and then supply an integer when creating a class instance:

self.battery = Battery(75)

EDIT: Updated as per comments below.

EDIT 2: Example when only two options are allowed

def __init__(self, battery_size: int):
    if isinstance(battery_size, int) and (battery_size in [75, 100]):
        self.battery_size = battery_size
    else:
        raise NotImplementedError('We do not produce this size of batteries yet') 

Here battery size will only allow 2 options, but if you decide to manually change the value of instance variable, you'll be able to set it to any value:

bat = Battery(75)
bat.battery_size = 34.5
# or even
bat.battery_size = True

To prevent this from happening a property can be used with a setter method that will do the required checks every time the value is set:

class Battery:

    def __init__(self, battery_size):
        self.battery_size = battery_size

    @property
    def battery_size(self):
        return self._battery_size

    @battery_size.setter
    def battery_size(self, value):
        if isinstance(value, int) and (value in [75, 100]):
            self._battery_size = value
        else:
            raise NotImplementedError('We do not produce this size of batteries yet')     

With this construct you will not be able to accidentally set it to a value other than 75 or 100.

pavel
  • 2,839
  • 2
  • 21
  • 33
  • Hi, @pavel, what if I wanted to have 2 options defined as arguments when defining the class? – champ885 Nov 24 '20 at 00:39
  • 1
    @champ885, Put information in the class' docstring, move the value check into the `__init__()` method or use `@property` and a setter method, but this might be a slightly more advanced method. I'll update the answer with the example. – pavel Nov 24 '20 at 01:41