0

I know that Python is a dynamically typed language, and that I am likely trying to recreate Java behavior here. However, I have a team of people working on this code base, and my goal with the code is to ensure that they are doing things in a consistent manner. Let me give an example:

class Company: def __init__(self, j): self.locations = [] 

When they instantiate a Company object, an empty list that holds locations is created. Now, with Python anything can be added to the list. However, I would like for this list to only contain Location objects:

class Location: def __init__(self, j): self.address = None self.city = None self.state = None self.zip = None 

I'm doing this with classes so that the code is self documenting. In other words, "location has only these attributes". My goal is that they do this:

c = Company() l = Location() l.city = "New York" c.locations.append(l) 

Unfortunately, nothing is stopping them from simply doing c.locations.append("foo"), and nothing indicates to them that c.locations should be a list of Location objects.

What is the Pythonic way to enforce consistency when working with a team of developers?

5
  • 2
    You can use type hints and run a static analyser over the code, e.g. mypy. But this will not prevent runtime type errors. You could also look into dataclasses Commented Feb 13, 2019 at 21:54
  • Did you look at e.g. this discussion : stackoverflow.com/questions/12569018/… ? Commented Feb 13, 2019 at 21:54
  • 1
    c.locations.append is bad. Provide a method on your object that checks the type before appending. Users of this class should only use that method and shouldn't be touching self.locations directly. Conventionally, attributes that aren't part of the public API are marked with a single-underscore, so you could use ._locations to let [people know: "don't touch this" Commented Feb 13, 2019 at 21:55
  • @Demi-Lune The advantage that I get is that doing a l. in VS Code, PyCharm, etc, shows the end user what attributes I expect them to set. I did not realize that they could add any attribute they wanted, which is unfortunate. Commented Feb 13, 2019 at 21:57
  • Also note that the type enforcement prevents duck typing (stackoverflow.com/questions/4205130/what-is-duck-typing), which is a nice feature of python (imho). So the pythonic answer could be "make a nice developer's doc". Commented Feb 13, 2019 at 22:29

2 Answers 2

4

An OOP solution is to make sure the users of your class' API do not have to interact directly with your instance attributes.

Methods

One approach is to implement methods which encapsulate the logic of adding a location.

Example

class Company: def __init__(self, j): self.locations = [] def add_location(self, location): if isinstance(location, Location): self.locations.append(location) else: raise TypeError("argument 'location' should be a Location object") 

Properties

Another OOP concept you can use is a property. Properties are a simple way to define getter and setters for your instance attributes.

Example

Suppose we want to enforce a certain format for a Location.zip attribute

class Location: def __init__(self): self._zip = None @property def zip(self): return self._zip @zip.setter def zip(self, value): if some_condition_on_value: self._zip = value else: raise ValueError('Incorrect format') @zip.deleter def zip(self): self._zip = None 

Notice that the attribute Location()._zip is still accessible and writable. While the underscore denotes what should be a private attribute, nothing is really private in Python.

Final word

Due to Python's high introspection capabilities, nothing will ever be totally safe. You will have to sit down with your team and discuss the tools and practice you want to adopt.

Nothing is really private in python. No class or class instance can keep you away from all what's inside (this makes introspection possible and powerful). Python trusts you. It says "hey, if you want to go poking around in dark places, I'm gonna trust that you've got a good reason and you're not making trouble."

After all, we're all consenting adults here.

--- Karl Fast

Sign up to request clarification or add additional context in comments.

6 Comments

Ah, I thought it was not Pythonic to have setters/getters. I guess it is different if the attribute is a list rather than a string?
@FranzKafka It is perfectly pythonic. Python actually has a whole getter and setter protocol which you should definitely know about: stackoverflow.com/questions/17330160/… I believe it will greatly help you with the issues you are facing.
@FranzKafka this is not a setter. A setter would be something like def set_locations(self, loc): self.loctions = loc You shouldn't do that. You should use a regular attribute and of you want to control access at some point, you can re-factor using a property
@FranzKafka no, not at all. If you just want to set or get an instance attribute, then you just do it. Directly mutating the object referenced by an instance attribute is another matter.
@property means the setter/getter implementation details are not exposed to the user, e.g. obj.var = "foo" works in both cases (setter function or basic attribute). But obj.var.append() is not the same thing.
|
1

You could also define a new class ListOfLocations that make the safety checks. Something like this

class ListOfLocations(list): def append(self,l): if not isinstance(l, Location): raise TypeError("Location required here") else: super().append(l) 

2 Comments

This is a good step, but does not prevent from using ListOfLocations.extend, ListOfLocations.insert, ListOfLocations.__setitem__, etc.
Yes, it'd require redefining any syntax that we want to protect. It's probably a matter of taste with your answer. The only advantage of this one, is that it doesn't require any modification of the initial test-case.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.