The error occurs because you have head.next = newval where newval is a number, but you should be assigning a node instance, i.e. newnode. So the next time you iterate the list, like in print_list you'll bump into that integer, and access its data attribute, as if it is a node...
Some other remarks on your code:
Your linkedlist class has no state. It merely provides some functions that act on the head argument. The OOP way to do this, is to give a linkedlist instance its own head attribute. And then rely on the self argument, which in your current code is not used in the functions.
Your main code should not have to deal with node instances. It should just have to create a linkedlist, and then populate it by calling its method(s). For that to work you'll have to deal with the case where the list is empty, and a first value is added to it, because then its head attribute needs to be set to it.
Use PascalCase for naming your classes.
The name of the print_list function is misleading: it doesn't print, but returns a list. So I would name it to_list instead.
Don't print the result of calling vals.add_at_end, because it is not designed to return anything, so you'll be printing None.
Corrected:
class Node(): # Use PascalCase
def __init__(self, data):
self.data = data
self.next = None
class LinkedList():
def __init__(self):
self.head = None # Give each instance its own head attribute
def add_at_end(self, newval):
if not self.head: # Deal with this case
self.head = Node(newval)
else:
head = self.head # Use attribute
while head.next:
head = head.next
head.next = Node(newval) # corrected
def to_list(self): # Better name
LL = []
head = self.head # Use attribute
while head:
LL.append(head.data)
head = head.next
return LL
vals = LinkedList()
vals.add_at_end(5) # Use method to add nodes
vals.add_at_end(6)
vals.add_at_end(2)
vals.add_at_end(8) # Nothing to print here
print(vals.to_list())